diff --git a/submodules/DrawingUI/Sources/DrawingWeatherEntityView.swift b/submodules/DrawingUI/Sources/DrawingWeatherEntityView.swift index b1fda8f725..05d6165b24 100644 --- a/submodules/DrawingUI/Sources/DrawingWeatherEntityView.swift +++ b/submodules/DrawingUI/Sources/DrawingWeatherEntityView.swift @@ -113,12 +113,7 @@ public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelega var result = self.textView.sizeThatFits(CGSize(width: self.weatherEntity.width, height: .greatestFiniteMagnitude)) self.textSize = result - let widthExtension: CGFloat - if self.weatherEntity.icon != nil { - widthExtension = result.height * 0.77 - } else { - widthExtension = result.height * 0.65 - } + let widthExtension: CGFloat = result.height * 0.7 result.width = floorToScreenPixels(max(224.0, ceil(result.width) + 20.0) + widthExtension) result.height = ceil(result.height * 1.2); return result; @@ -136,19 +131,17 @@ public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelega public override func layoutSubviews() { super.layoutSubviews() - let iconSize: CGFloat - let iconOffset: CGFloat - if self.weatherEntity.icon != nil { - iconSize = min(80.0, floor(self.bounds.height * 0.7)) - iconOffset = 0.2 - } else { - iconSize = min(76.0, floor(self.bounds.height * 0.6)) - iconOffset = 0.3 - } - + let iconSize = min(80.0, floor(self.bounds.height * 0.7)) + let iconOffset: CGFloat = 0.3 + self.iconView.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(iconSize * iconOffset), y: floorToScreenPixels((self.bounds.height - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize)) self.imageNode.frame = self.iconView.frame.offsetBy(dx: 0.0, dy: 2.0) + if let animationNode = self.animationNode { + animationNode.frame = self.iconView.frame.offsetBy(dx: 0.0, dy: 2.0) + animationNode.updateLayout(size: self.iconView.frame.size) + } + let imageSize = CGSize(width: iconSize, height: iconSize) self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() @@ -304,29 +297,19 @@ public final class DrawingWeatherEntityView: DrawingEntityView, UITextViewDelega self.addSubnode(self.imageNode) if let dimensions = file.dimensions { if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" { + let fittedDimensions = dimensions.cgSize.aspectFitted(CGSize(width: 256.0, height: 256.0)) if self.animationNode == nil { let animationNode = DefaultAnimatedStickerNodeImpl() - animationNode.autoplay = false + animationNode.autoplay = true self.animationNode = animationNode - animationNode.started = { [weak self, weak animationNode] in + animationNode.started = { [weak self] in self?.imageNode.isHidden = true - - let _ = animationNode -// if let animationNode = animationNode { -// let _ = (animationNode.status -// |> take(1) -// |> deliverOnMainQueue).start(next: { [weak self] status in -// self?.started?(status.duration) -// }) -// } } - self.addSubnode(animationNode) + animationNode.setup(source: AnimatedStickerResourceSource(account: self.context.account, resource: file.resource, isVideo: file.isVideoSticker), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil)) - if file.isCustomTemplateEmoji { - animationNode.dynamicColor = UIColor(rgb: 0xffffff) - } + self.addSubnode(animationNode) } - self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, userLocation: .other, file: file, small: false, size: dimensions.cgSize.aspectFitted(CGSize(width: 256.0, height: 256.0)))) + self.imageNode.setSignal(chatMessageAnimatedSticker(postbox: self.context.account.postbox, userLocation: .other, file: file, small: false, size: fittedDimensions)) self.stickerFetchedDisposable.set(freeMediaFileResourceInteractiveFetched(account: self.context.account, userLocation: .other, fileReference: stickerPackFileReference(file), resource: file.resource).start()) } else { if let animationNode = self.animationNode { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift index b38a45fcad..6a2d3760f3 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift @@ -28,6 +28,10 @@ private func prerenderTextTransformations(entity: DrawingEntity, image: UIImage, angle = -entity.rotation scale = entity.scale position = entity.position + } else if let entity = entity as? DrawingWeatherEntity { + angle = -entity.rotation + scale = entity.scale + position = entity.position } else if let entity = entity as? DrawingLinkEntity { angle = -entity.rotation scale = entity.scale @@ -122,6 +126,8 @@ func composerEntitiesForDrawingEntity(postbox: Postbox, textScale: CGFloat, enti return entities } else if let entity = entity as? DrawingLocationEntity { return [prerenderTextTransformations(entity: entity, image: renderImage, textScale: textScale, colorSpace: colorSpace)] + } else if let entity = entity as? DrawingWeatherEntity { + return [prerenderTextTransformations(entity: entity, image: renderImage, textScale: textScale, colorSpace: colorSpace)] } else if let entity = entity as? DrawingLinkEntity { return [prerenderTextTransformations(entity: entity, image: renderImage, textScale: textScale, colorSpace: colorSpace)] } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index a7d31ae2bc..c5f8979db7 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -63,6 +63,7 @@ swift_library( "//submodules/TelegramUI/Components/ListSectionComponent", "//submodules/WebsiteType", "//submodules/UrlEscaping", + "//submodules/DeviceLocationManager", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 495bf3f24f..28b3aac8a4 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -2545,6 +2545,16 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private(set) var hasAnyChanges = false + fileprivate var drawingScreen: DrawingScreen? + fileprivate var stickerScreen: StickerPickerScreen? + fileprivate weak var cutoutScreen: MediaCutoutScreen? + private var defaultToEmoji = false + + private var previousDrawingData: Data? + private var previousDrawingEntities: [DrawingEntity]? + + private var weatherPromise: Promise? + private var playbackPositionDisposable: Disposable? var recording: MediaEditorScreen.Recording @@ -4572,61 +4582,21 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.mediaEditor?.play() } - func addWeather() { - if !self.didSetupStaticEmojiPack { - self.didSetupStaticEmojiPack = true - self.staticEmojiPack.set(self.context.engine.stickers.loadedStickerPack(reference: .name("staticemoji"), forceActualized: false)) - } + func addWeather(_ weather: StickerPickerScreen.Weather.LoadedWeather) { + let weatherFormatter = MeasurementFormatter() + weatherFormatter.locale = Locale.current + weatherFormatter.unitStyle = .short + weatherFormatter.numberFormatter.maximumFractionDigits = 0 - let emojiFile: Signal - let emoji = "☀️".strippedEmoji - - emojiFile = self.context.animatedEmojiStickers - |> take(1) - |> map { result -> TelegramMediaFile? in - if let file = result[emoji]?.first { - return file.file - } else { - return nil - } - -// if case let .result(_, items, _) = result, let match = items.first(where: { item in -// var displayText: String? -// for attribute in item.file.attributes { -// if case let .Sticker(alt, _, _) = attribute { -// displayText = alt -// break -// } -// } -// if let displayText, displayText.hasPrefix(emoji) { -// return true -// } else { -// return false -// } -// }) { -// return match.file -// } else { -// return nil -// } - } - - - let _ = (emojiFile - |> deliverOnMainQueue).start(next: { [weak self] emojiFile in - guard let self else { - return - } - let scale = 1.0 - self.interaction?.insertEntity( - DrawingWeatherEntity( - temperature: "35°C", - style: .white, - icon: emojiFile - ), - scale: scale, - position: nil - ) - }) + self.interaction?.insertEntity( + DrawingWeatherEntity( + temperature: weatherFormatter.string(from: Measurement(value: weather.temperature, unit: UnitTemperature.celsius)), + style: .white, + icon: weather.emojiFile + ), + scale: nil, + position: nil + ) } func updateModalTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) { @@ -4707,15 +4677,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate func viewForZooming(in scrollView: UIScrollView) -> UIView? { return self.previewContentContainerView } - - fileprivate var drawingScreen: DrawingScreen? - fileprivate var stickerScreen: StickerPickerScreen? - fileprivate weak var cutoutScreen: MediaCutoutScreen? - private var defaultToEmoji = false - - private var previousDrawingData: Data? - private var previousDrawingEntities: [DrawingEntity]? - + func requestLayout(forceUpdate: Bool, transition: ComponentTransition) { guard let layout = self.validLayout else { return @@ -4812,7 +4774,23 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let controller = self.controller, case .stickerEditor = controller.mode { hasInteractiveStickers = false } - let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get(), forceDark: true, defaultToEmoji: self.defaultToEmoji, hasGifs: true, hasInteractiveStickers: hasInteractiveStickers) + + var weatherSignal: Signal + if "".isEmpty { + let weatherPromise: Promise + if let current = self.weatherPromise { + weatherPromise = current + } else { + weatherPromise = Promise() + weatherPromise.set(getWeather(context: self.context)) + self.weatherPromise = weatherPromise + } + weatherSignal = weatherPromise.get() + } else { + weatherSignal = .single(.none) + } + + let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get(), forceDark: true, defaultToEmoji: self.defaultToEmoji, hasGifs: true, hasInteractiveStickers: hasInteractiveStickers, weather: weatherSignal) controller.completion = { [weak self] content in guard let self else { return false @@ -4885,7 +4863,14 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } controller.addWeather = { [weak self, weak controller] in if let self { - self.addWeather() + if let weatherPromise = self.weatherPromise { + let _ = (weatherPromise.get() + |> take(1)).start(next: { [weak self] weather in + if let self, case let .loaded(loaded) = weather { + self.addWeather(loaded) + } + }) + } self.stickerScreen = nil controller?.dismiss(animated: true) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorUtils.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorUtils.swift index a3bd79bbf6..b87eef93c0 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorUtils.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorUtils.swift @@ -1,16 +1,21 @@ import Foundation +import CoreLocation +import SwiftSignalKit +import TelegramCore +import StickerPickerScreen +import AccountContext +import DeviceLocationManager -func emojiFor(for meteocode: Int, timestamp: Int32) -> String { +func emojiFor(for meteocode: Int, date: Date, location: CLLocationCoordinate2D) -> String? { var emoji = weatherEmoji(for: meteocode) - if ["☀️", "🌤️"].contains(emoji) { - emoji = moonPhaseEmoji(for: timestamp) + if ["☀️", "🌤️"].contains(emoji) && isNightTime(date: date, location: location) { + emoji = moonPhaseEmoji(for: date) } return emoji } -func moonPhaseEmoji(for timestamp: Int32) -> String { +private func moonPhaseEmoji(for date: Date) -> String { let newMoonDate = Date(timeIntervalSince1970: 1612137600) - let date = Date(timeIntervalSince1970: Double(timestamp)) let lunarMonth: TimeInterval = 29.53058867 * 24 * 60 * 60 let daysSinceNewMoon = date.timeIntervalSince(newMoonDate) / (24 * 60 * 60) @@ -38,7 +43,7 @@ func moonPhaseEmoji(for timestamp: Int32) -> String { } } -func weatherEmoji(for meteocode: Int) -> String { +func weatherEmoji(for meteocode: Int) -> String? { switch meteocode { case 0: return "☀️" @@ -65,6 +70,188 @@ func weatherEmoji(for meteocode: Int) -> String { case 95, 96, 99: return "⛈️" // Thunderstorm: Slight or moderate default: - return "❓" + return nil } } + +struct StoryWeather { + let emoji: String + let temperature: Double +} + +private func getWeatherData(location: CLLocationCoordinate2D) -> Signal { + let latitude = "\(location.latitude)" + let longitude = "\(location.longitude)" + let url = "https://api.open-meteo.com/v1/forecast?latitude=\(latitude)&longitude=\(longitude)¤t=temperature_2m,weather_code" + + return Signal { subscriber in + let disposable = fetchHttpResource(url: url).start(next: { result in + if case let .dataPart(_, data, _, complete) = result, complete { + guard let dict = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] else { + subscriber.putNext(nil) + subscriber.putCompletion() + return + } + guard let current = dict["current"] as? [String: Any], let temperature = current["temperature_2m"] as? Double, let weatherCode = current["weather_code"] as? Int else { + subscriber.putNext(nil) + subscriber.putCompletion() + return + } + if let emoji = emojiFor(for: weatherCode, date: Date(), location: location) { + subscriber.putNext(StoryWeather(emoji: emoji, temperature: temperature)) + } else { + subscriber.putNext(nil) + } + subscriber.putCompletion() + } + }) + + return disposable + } +} + +func getWeather(context: AccountContext) -> Signal { + guard let locationManager = context.sharedContext.locationManager else { + return .single(.none) + } + return .single(.fetching) + |> then( + currentLocationManagerCoordinate(manager: locationManager, timeout: 5.0) + |> mapToSignal { location in + if let location { + return getWeatherData(location: location) + |> mapToSignal { weather in + if let weather { + return context.animatedEmojiStickers + |> take(1) + |> mapToSignal { result in + if let match = result[weather.emoji.strippedEmoji]?.first { + return .single(.loaded(StickerPickerScreen.Weather.LoadedWeather( + emoji: weather.emoji.strippedEmoji, + emojiFile: match.file, + temperature: weather.temperature + ))) + } else { + return .single(.none) + } + } + } else { + return .single(.none) + } + } + } else { + return .single(.none) + } + } + ) +} + +private func calculateSunriseSunset(date: Date, location: CLLocationCoordinate2D) -> (sunrise: Date, sunset: Date)? { + guard let utcTimezone = TimeZone(identifier: "UTC") else { return nil } + + let zenith: Double = 90.83 + + var calendar = Calendar(identifier: .gregorian) + calendar.timeZone = utcTimezone + + guard let dayOfYear = calendar.ordinality(of: .day, in: .year, for: date) else { + return nil + } + + func toRadians(_ degrees: Double) -> Double { + return degrees * .pi / 180.0 + } + + func toDegrees(_ radians: Double) -> Double { + return radians * 180.0 / .pi + } + + func normalise(_ value: Double, maximum: Double) -> Double { + var value = value + if value < 0 { + value += maximum + } + if value > maximum { + value -= maximum + } + return value + } + + func calculateTime(isSunrise: Bool) -> Date? { + let day = Double(dayOfYear) + let lngHour = location.longitude / 15.0 + + let hourTime: Double = isSunrise ? 6 : 18 + let t = day + ((hourTime - lngHour) / 24) + + let M = (0.9856 * t) - 3.289 + + var L = M + 1.916 * sin(toRadians(M)) + 0.020 * sin(2 * toRadians(M)) + 282.634 + L = normalise(L, maximum: 360) + + var RA = toDegrees(atan(0.91764 * tan(toRadians(L)))) + RA = normalise(RA, maximum: 360) + + let Lquadrant = floor(L / 90) * 90 + let RAquadrant = floor(RA / 90) * 90 + RA = RA + (Lquadrant - RAquadrant) + RA = RA / 15 + + let sinDec = 0.39782 * sin(toRadians(L)) + let cosDec = cos(asin(sinDec)) + let cosH = (cos(toRadians(zenith)) - (sinDec * sin(toRadians(location.latitude)))) / (cosDec * cos(toRadians(location.latitude))) + guard cosH < 1 else { + return nil + } + guard cosH > -1 else { + return nil + } + + let tempH = isSunrise ? 360.0 - toDegrees(acos(cosH)) : toDegrees(acos(cosH)) + let H = tempH / 15.0 + let T = H + RA - (0.06571 * t) - 6.622 + + var UT = T - lngHour + UT = normalise(UT, maximum: 24) + + let hour = floor(UT) + let minute = floor((UT - hour) * 60.0) + let second = (((UT - hour) * 60) - minute) * 60.0 + + let shouldBeYesterday = lngHour > 0 && UT > 12 && isSunrise + let shouldBeTomorrow = lngHour < 0 && UT < 12 && !isSunrise + + let setDate: Date + if shouldBeYesterday { + setDate = Date(timeInterval: -(60 * 60 * 24), since: date) + } else if shouldBeTomorrow { + setDate = Date(timeInterval: (60 * 60 * 24), since: date) + } else { + setDate = date + } + + var components = calendar.dateComponents([.day, .month, .year], from: setDate) + components.hour = Int(hour) + components.minute = Int(minute) + components.second = Int(second) + + calendar.timeZone = utcTimezone + return calendar.date(from: components) + } + + guard let sunrise = calculateTime(isSunrise: true), + let sunset = calculateTime(isSunrise: false) else { + return nil + } + + return (sunrise, sunset) +} + +private func isNightTime(date: Date, location: CLLocationCoordinate2D) -> Bool { + let calendar = Calendar.current + let date = calendar.startOfDay(for: date) + guard let (sunrise, sunset) = calculateSunriseSunset(date: date, location: location) else { + return false + } + return date < sunrise || date > sunset +} diff --git a/submodules/TelegramUI/Components/StickerPickerScreen/BUILD b/submodules/TelegramUI/Components/StickerPickerScreen/BUILD index 1de601b336..a216f747cb 100644 --- a/submodules/TelegramUI/Components/StickerPickerScreen/BUILD +++ b/submodules/TelegramUI/Components/StickerPickerScreen/BUILD @@ -33,6 +33,8 @@ swift_library( "//submodules/TelegramUI/Components/EntityKeyboard", "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", "//submodules/TelegramUI/Components/ButtonComponent", + "//submodules/TelegramUI/Components/LottieComponent", + "//submodules/TelegramUI/Components/LottieComponentResourceContent", "//submodules/ChatPresentationInterfaceState", "//submodules/TelegramUI/Components/MediaEditor", "//submodules/TelegramUI/Components/CameraButtonComponent", diff --git a/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift index 969429e816..b5403e11e1 100644 --- a/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift +++ b/submodules/TelegramUI/Components/StickerPickerScreen/Sources/StickerPickerScreen.swift @@ -20,6 +20,8 @@ import MediaEditor import EntityKeyboardGifContent import CameraButtonComponent import BundleIconComponent +import LottieComponent +import LottieComponentResourceContent import UndoUI import GalleryUI @@ -532,7 +534,10 @@ public class StickerPickerScreen: ViewController { self.containerView.addSubview(self.hostView) if controller.hasInteractiveStickers { - self.storyStickersContentView = StoryStickersContentView(isPremium: context.isPremium) + self.storyStickersContentView = StoryStickersContentView( + context: context, + weather: controller.weather + ) self.storyStickersContentView?.locationAction = { [weak self] in self?.controller?.presentLocationPicker() } @@ -2044,15 +2049,34 @@ public class StickerPickerScreen: ViewController { return self.displayNode as! Node } + public enum Weather { + public struct LoadedWeather { + public let emoji: String + public let emojiFile: TelegramMediaFile + public let temperature: Double + + public init(emoji: String, emojiFile: TelegramMediaFile, temperature: Double) { + self.emoji = emoji + self.emojiFile = emojiFile + self.temperature = temperature + } + } + + case none + case fetching + case loaded(StickerPickerScreen.Weather.LoadedWeather) + } + private let context: AccountContext private let theme: PresentationTheme - fileprivate let forceDark: Bool + let forceDark: Bool private let inputData: Signal - fileprivate let defaultToEmoji: Bool + let defaultToEmoji: Bool let isFullscreen: Bool let hasEmoji: Bool let hasGifs: Bool let hasInteractiveStickers: Bool + let weather: Signal private var currentLayout: ContainerViewLayout? @@ -2068,7 +2092,7 @@ public class StickerPickerScreen: ViewController { public var addLink: () -> Void = { } public var addWeather: () -> Void = { } - public init(context: AccountContext, inputData: Signal, forceDark: Bool = false, expanded: Bool = false, defaultToEmoji: Bool = false, hasEmoji: Bool = true, hasGifs: Bool = false, hasInteractiveStickers: Bool = true) { + public init(context: AccountContext, inputData: Signal, forceDark: Bool = false, expanded: Bool = false, defaultToEmoji: Bool = false, hasEmoji: Bool = true, hasGifs: Bool = false, hasInteractiveStickers: Bool = true, weather: Signal = .single(.none)) { self.context = context let presentationData = context.sharedContext.currentPresentationData.with { $0 } self.theme = forceDark ? defaultDarkColorPresentationTheme : presentationData.theme @@ -2079,6 +2103,7 @@ public class StickerPickerScreen: ViewController { self.hasEmoji = hasEmoji self.hasGifs = hasGifs self.hasInteractiveStickers = hasInteractiveStickers + self.weather = weather super.init(navigationBarPresentationData: expanded ? NavigationBarPresentationData(presentationData: presentationData) : nil) @@ -2141,22 +2166,28 @@ public class StickerPickerScreen: ViewController { } private final class InteractiveStickerButtonContent: Component { + let context: AccountContext let theme: PresentationTheme - let title: String - let iconName: String + let title: String? + let iconName: String? + let iconFile: TelegramMediaFile? let useOpaqueTheme: Bool weak var tintContainerView: UIView? public init( + context: AccountContext, theme: PresentationTheme, - title: String, - iconName: String, + title: String?, + iconName: String?, + iconFile: TelegramMediaFile? = nil, useOpaqueTheme: Bool, tintContainerView: UIView ) { + self.context = context self.theme = theme self.title = title self.iconName = iconName + self.iconFile = iconFile self.useOpaqueTheme = useOpaqueTheme self.tintContainerView = tintContainerView } @@ -2207,68 +2238,80 @@ private final class InteractiveStickerButtonContent: Component { func update(component: InteractiveStickerButtonContent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: ComponentTransition) -> CGSize { self.backgroundLayer.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.11).cgColor - + let iconSize: CGSize - if component.iconName == "Sun" { - iconSize = self.icon.update( + let buttonSize: CGSize + if let title = component.title { + if let iconFile = component.iconFile { + iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent( + LottieComponent( + content: LottieComponent.ResourceContent(context: component.context, file: iconFile, attemptSynchronously: true, providesPlaceholder: true), + color: nil, + placeholderColor: UIColor(rgb: 0xffffff, alpha: 0.4), + loop: !["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘"].contains(component.iconName ?? "") + ) + ), + environment: {}, + containerSize: CGSize(width: 20.0, height: 20.0) + ) + } else { + iconSize = self.icon.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent( + name: component.iconName ?? "", + tintColor: .white, + maxSize: CGSize(width: 20.0, height: 20.0) + )), + environment: {}, + containerSize: availableSize + ) + } + let titleSize = self.title.update( transition: .immediate, component: AnyComponent(Text( - text: "☀️", + text: title.uppercased(), font: Font.with(size: 23.0, design: .camera), color: .white )), environment: {}, containerSize: availableSize ) + + let padding: CGFloat = 7.0 + let spacing: CGFloat = 4.0 + buttonSize = CGSize(width: padding + iconSize.width + spacing + titleSize.width + padding, height: 34.0) + + if let view = self.icon.view { + if view.superview == nil { + self.addSubview(view) + } + transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: padding, y: floorToScreenPixels((buttonSize.height - iconSize.height) / 2.0)), size: iconSize)) + } + if let view = self.title.view { + if view.superview == nil { + self.addSubview(view) + } + transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: padding + iconSize.width + spacing, y: floorToScreenPixels((buttonSize.height - titleSize.height) / 2.0)), size: titleSize)) + } } else { - iconSize = self.icon.update( - transition: .immediate, - component: AnyComponent(BundleIconComponent( - name: component.iconName, - tintColor: .white, - maxSize: CGSize(width: 20.0, height: 20.0) - )), - environment: {}, - containerSize: availableSize - ) - } - let titleSize = self.title.update( - transition: .immediate, - component: AnyComponent(Text( - text: component.title.uppercased(), - font: Font.with(size: 23.0, design: .camera), - color: .white - )), - environment: {}, - containerSize: availableSize - ) - - let padding: CGFloat = 7.0 - let spacing: CGFloat = 4.0 - let buttonSize = CGSize(width: padding + iconSize.width + spacing + titleSize.width + padding, height: 34.0) - - if let view = self.icon.view { - if view.superview == nil { - self.addSubview(view) - } - transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: padding, y: floorToScreenPixels((buttonSize.height - iconSize.height) / 2.0)), size: iconSize)) - } - if let view = self.title.view { - if view.superview == nil { - self.addSubview(view) - } - transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: padding + iconSize.width + spacing, y: floorToScreenPixels((buttonSize.height - titleSize.height) / 2.0)), size: titleSize)) + buttonSize = CGSize(width: 87.0, height: 34.0) } self.backgroundLayer.cornerRadius = 6.0 self.tintBackgroundLayer.cornerRadius = 6.0 - self.backgroundLayer.frame = CGRect(origin: .zero, size: buttonSize) + var transition = transition + if self.backgroundLayer.frame.width.isZero { + transition = .immediate + } + transition.setFrame(layer: self.backgroundLayer, frame: CGRect(origin: .zero, size: buttonSize)) if self.tintBackgroundLayer.superlayer == nil, let tintContainerView = component.tintContainerView { Queue.mainQueue().justDispatch { let mappedFrame = self.convert(self.bounds, to: tintContainerView) - self.tintBackgroundLayer.frame = mappedFrame + transition.setFrame(layer: self.tintBackgroundLayer, frame: mappedFrame) } } @@ -2441,12 +2484,14 @@ final class ItemStack: CombinedComponent { private let padding: CGFloat private let minSpacing: CGFloat private let verticalSpacing: CGFloat + private let maxHorizontalItems: Int - init(_ items: [AnyComponentWithIdentity], padding: CGFloat, minSpacing: CGFloat, verticalSpacing: CGFloat) { + init(_ items: [AnyComponentWithIdentity], padding: CGFloat, minSpacing: CGFloat, verticalSpacing: CGFloat, maxHorizontalItems: Int) { self.items = items self.padding = padding self.minSpacing = minSpacing self.verticalSpacing = verticalSpacing + self.maxHorizontalItems = maxHorizontalItems } static func ==(lhs: ItemStack, rhs: ItemStack) -> Bool { @@ -2462,6 +2507,9 @@ final class ItemStack: CombinedComponent { if lhs.verticalSpacing != rhs.verticalSpacing { return false } + if lhs.maxHorizontalItems != rhs.maxHorizontalItems { + return false + } return true } @@ -2491,7 +2539,7 @@ final class ItemStack: CombinedComponent { let remainingWidth = context.availableSize.width - itemsWidth - context.component.padding * 2.0 let spacing = remainingWidth / CGFloat(rowItemsCount - 1) - if spacing < context.component.minSpacing || currentGroup.count == 3 { + if spacing < context.component.minSpacing || currentGroup.count == context.component.maxHorizontalItems { groups.append(currentGroup) currentGroup = [] } @@ -2545,148 +2593,233 @@ final class ItemStack: CombinedComponent { } final class StoryStickersContentView: UIView, EmojiCustomContentView { + private let context: AccountContext + private let weatherFormatter: MeasurementFormatter + let tintContainerView = UIView() - private let container = ComponentView() - private let isPremium: Bool + private var weatherDisposable: Disposable? + private var weather: StickerPickerScreen.Weather = .none var locationAction: () -> Void = {} var audioAction: () -> Void = {} var reactionAction: () -> Void = {} var linkAction: () -> Void = {} var weatherAction: () -> Void = {} + + private var params: (theme: PresentationTheme, strings: PresentationStrings, useOpaqueTheme: Bool, availableSize: CGSize)? - init(isPremium: Bool) { - self.isPremium = isPremium + init(context: AccountContext, weather: Signal) { + self.context = context + self.weatherFormatter = MeasurementFormatter() + self.weatherFormatter.locale = Locale.current + self.weatherFormatter.unitStyle = .short + self.weatherFormatter.numberFormatter.maximumFractionDigits = 0 super.init(frame: .zero) + + self.weatherDisposable = (weather + |> deliverOnMainQueue).start(next: { [weak self] weather in + guard let self else { + return + } + self.weather = weather + if let (theme, strings, useOpaqueTheme, availableSize) = self.params { + let _ = self.update(theme: theme, strings: strings, useOpaqueTheme: useOpaqueTheme, availableSize: availableSize, transition: .easeInOut(duration: 0.25)) + } + }) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } + deinit { + self.weatherDisposable?.dispose() + } + func update(theme: PresentationTheme, strings: PresentationStrings, useOpaqueTheme: Bool, availableSize: CGSize, transition: ComponentTransition) -> CGSize { + self.params = (theme, strings, useOpaqueTheme, availableSize) + let padding: CGFloat = 22.0 + var maxHorizontalItems = 2 + var items: [AnyComponentWithIdentity] = [] + items.append( + AnyComponentWithIdentity( + id: "link", + component: AnyComponent( + CameraButton( + content: AnyComponentWithIdentity( + id: "content", + component: AnyComponent( + InteractiveStickerButtonContent( + context: self.context, + theme: theme, + title: strings.MediaEditor_AddLink, + iconName: self.context.isPremium ? "Media Editor/Link" : "Media Editor/LinkLocked", + useOpaqueTheme: useOpaqueTheme, + tintContainerView: self.tintContainerView + ) + ) + ), + action: { [weak self] in + if let self { + self.linkAction() + } + }) + ) + ) + ) + items.append( + AnyComponentWithIdentity( + id: "location", + component: AnyComponent( + CameraButton( + content: AnyComponentWithIdentity( + id: "content", + component: AnyComponent( + InteractiveStickerButtonContent( + context: self.context, + theme: theme, + title: strings.MediaEditor_AddLocationShort, + iconName: "Chat/Attach Menu/Location", + useOpaqueTheme: useOpaqueTheme, + tintContainerView: self.tintContainerView + ) + ) + ), + action: { [weak self] in + if let self { + self.locationAction() + } + }) + ) + ) + ) + + if case .none = self.weather { + + } else { + maxHorizontalItems = 3 + + switch self.weather { + case let .loaded(weather): + items.append( + AnyComponentWithIdentity( + id: "weather", + component: AnyComponent( + CameraButton( + content: AnyComponentWithIdentity( + id: "weather", + component: AnyComponent( + InteractiveStickerButtonContent( + context: self.context, + theme: theme, + title: self.weatherFormatter.string(from: Measurement(value: weather.temperature, unit: UnitTemperature.celsius)), + iconName: weather.emoji, + iconFile: weather.emojiFile, + useOpaqueTheme: useOpaqueTheme, + tintContainerView: self.tintContainerView + ) + ) + ), + action: { [weak self] in + if let self { + self.weatherAction() + } + }) + ) + ) + ) + case .fetching: + items.append( + AnyComponentWithIdentity( + id: "weather", + component: AnyComponent( + CameraButton( + content: AnyComponentWithIdentity( + id: "weather", + component: AnyComponent( + InteractiveStickerButtonContent( + context: self.context, + theme: theme, + title: nil, + iconName: nil, + useOpaqueTheme: useOpaqueTheme, + tintContainerView: self.tintContainerView + ) + ) + ), + action: { [weak self] in + if let self { + self.weatherAction() + } + }) + ) + ) + ) + default: + fatalError() + } + } + + items.append( + AnyComponentWithIdentity( + id: "audio", + component: AnyComponent( + CameraButton( + content: AnyComponentWithIdentity( + id: "audio", + component: AnyComponent( + InteractiveStickerButtonContent( + context: self.context, + theme: theme, + title: strings.MediaEditor_AddAudio, + iconName: "Media Editor/Audio", + useOpaqueTheme: useOpaqueTheme, + tintContainerView: self.tintContainerView + ) + ) + ), + action: { [weak self] in + if let self { + self.audioAction() + } + }) + ) + ) + ) + + items.append( + AnyComponentWithIdentity( + id: "reaction", + component: AnyComponent( + CameraButton( + content: AnyComponentWithIdentity( + id: "reaction", + component: AnyComponent( + InteractiveReactionButtonContent(theme: theme) + ) + ), + action: { [weak self] in + if let self { + self.reactionAction() + } + }) + ) + ) + ) + let size = self.container.update( transition: transition, component: AnyComponent( ItemStack( - [ - AnyComponentWithIdentity( - id: "link", - component: AnyComponent( - CameraButton( - content: AnyComponentWithIdentity( - id: "content", - component: AnyComponent( - InteractiveStickerButtonContent( - theme: theme, - title: strings.MediaEditor_AddLink, - iconName: self.isPremium ? "Media Editor/Link" : "Media Editor/LinkLocked", - useOpaqueTheme: useOpaqueTheme, - tintContainerView: self.tintContainerView - ) - ) - ), - action: { [weak self] in - if let self { - self.linkAction() - } - }) - ) - ), - AnyComponentWithIdentity( - id: "location", - component: AnyComponent( - CameraButton( - content: AnyComponentWithIdentity( - id: "content", - component: AnyComponent( - InteractiveStickerButtonContent( - theme: theme, - title: strings.MediaEditor_AddLocationShort, - iconName: "Chat/Attach Menu/Location", - useOpaqueTheme: useOpaqueTheme, - tintContainerView: self.tintContainerView - ) - ) - ), - action: { [weak self] in - if let self { - self.locationAction() - } - }) - ) - ), - AnyComponentWithIdentity( - id: "weather", - component: AnyComponent( - CameraButton( - content: AnyComponentWithIdentity( - id: "weather", - component: AnyComponent( - InteractiveStickerButtonContent( - theme: theme, - title: "35°C", - iconName: "Sun", - useOpaqueTheme: useOpaqueTheme, - tintContainerView: self.tintContainerView - ) - ) - ), - action: { [weak self] in - if let self { - self.weatherAction() - } - }) - ) - ), - AnyComponentWithIdentity( - id: "audio", - component: AnyComponent( - CameraButton( - content: AnyComponentWithIdentity( - id: "audio", - component: AnyComponent( - InteractiveStickerButtonContent( - theme: theme, - title: strings.MediaEditor_AddAudio, - iconName: "Media Editor/Audio", - useOpaqueTheme: useOpaqueTheme, - tintContainerView: self.tintContainerView - ) - ) - ), - action: { [weak self] in - if let self { - self.audioAction() - } - }) - ) - ), - AnyComponentWithIdentity( - id: "reaction", - component: AnyComponent( - CameraButton( - content: AnyComponentWithIdentity( - id: "reaction", - component: AnyComponent( - InteractiveReactionButtonContent(theme: theme) - ) - ), - action: { [weak self] in - if let self { - self.reactionAction() - } - }) - ) - ) - ], + items, padding: 18.0, minSpacing: 8.0, - verticalSpacing: 12.0 + verticalSpacing: 12.0, + maxHorizontalItems: maxHorizontalItems ) ), environment: {},