diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 168eade6ca..40293ad6d5 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9712,6 +9712,7 @@ Sorry for the inconvenience."; "Story.Privacy.PostStory" = "Post Story"; "Story.Views.ViewsExpired" = "List of viewers becomes unavailable **24 hours** after the story expires."; +"Story.Views.ViewsNotRecorded" = "Information about viewers wasn’t recorded."; "Story.Views.NoViews" = "Nobody has viewed\nyour story yet."; "AutoDownloadSettings.Stories" = "Stories"; @@ -9756,10 +9757,35 @@ Sorry for the inconvenience."; "Premium.New" = "NEW"; "MediaEditor.AddGif" = "Add GIF"; +"MediaEditor.AddLocation" = "Add Location"; "Premium.Stories" = "Upgraded Stories"; "Premium.StoriesInfo" = "Priority order, stealth mode, permanent views history and more."; +"Premium.Stories.Title" = "Upgraded Stories"; +"Premium.Stories.AdditionalTitle" = "Exclusive Features in Stories"; + +"Premium.Stories.Order.Title" = "Priority Order"; +"Premium.Stories.Order.Text" = "Get more views as your stories are always displayed first."; + +"Premium.Stories.Stealth.Title" = "Stealth Mode"; +"Premium.Stories.Stealth.Text" = "Hide the fact that you viewed other people's stories."; + +"Premium.Stories.Views.Title" = "Permanent Views History"; +"Premium.Stories.Views.Text" = "Check who opens your stories — even after they expire."; + +"Premium.Stories.Expiration.Title" = "Expiration Durations"; +"Premium.Stories.Expiration.Text" = "Set custom expiration durations like 6 or 48 hours for your stories."; + +"Premium.Stories.Save.Title" = "Save Stories to Gallery"; +"Premium.Stories.Save.Text" = "Save other people's unprotected stories to your Gallery."; + +"Premium.Stories.Captions.Title" = "Longer Captions"; +"Premium.Stories.Captions.Text" = "Add ten times longer captions to your stories."; + +"Premium.Stories.Format.Title" = "Links and Formatting"; +"Premium.Stories.Format.Text" = "Add links and formatting in captions to your stories."; + "Premium.MaxExpiringStoriesText" = "You can post **%@** stories in **24** hours. Subscribe to **Telegram Premium** to increase this limit to **%@**."; "Premium.MaxExpiringStoriesNoPremiumText" = "You have reached the limit of **%@** stories per **24** hours."; "Premium.MaxExpiringStoriesFinalText" = "You have reached the limit of **%@** stories per **24** hours."; @@ -9771,3 +9797,62 @@ Sorry for the inconvenience."; "Premium.MaxStoriesMonthlyText" = "You can post **%@** stories in a month. Upgrade to **Telegram Premium** to increase this limit to **%@**."; "Premium.MaxStoriesMonthlyNoPremiumText" = "You have reached the limit of **%@** stories per month."; "Premium.MaxStoriesMonthlyFinalText" = "You have reached the limit of **%@** stories per month."; + +"MediaPicker.Recents" = "Recents"; + +"Story.LongTapForMoreReactions" = "Long tap for more reactions"; +"Story.StealthModeActivePlaceholder" = "Stealth Mode active – %@"; + +"Story.ContextShowStoriesTo" = "Show My Stories To %@"; +"Story.ToastShowStoriesTo" = "**%@** will now see your stories."; +"Story.ContextHideStoriesFrom" = "Hide My Stories From %@"; +"Story.ToastHideStoriesFrom" = "**%@** will not see your stories anymore."; +"Story.ContextDeleteContact" = "Delete Contact"; +"Story.ToastDeletedContact" = "**%@** has been removed from your contacts."; +"Story.ToastUserBlocked" = "**%@** has been blocked."; +"Story.ToastPremiumSaveToGallery" = "Subscribe to [Telegram Premium]() to save other people's unprotected stories to your Gallery."; +"Story.PremiumUpgradeStoriesButton" = "Upgrade Stories"; +"Story.ContextStealthMode" = "Stealth Mode"; +"Story.AlertStealthModeActiveTitle" = "You are in Stealth Mode now"; +"Story.AlertStealthModeActiveText" = "If you send a reply or reaction, the creator of the story will also see you in the list of viewers."; +"Story.AlertStealthModeActiveAction" = "Proceed"; +"Story.ToastStealthModeActiveTitle" = "You are in Stealth Mode now"; +"Story.ToastStealthModeActiveText" = "The creators of stories you will view in the next **%@** won't see you in the viewers' lists."; +"Story.ToastStealthModeActivatedTitle" = "Stealth Mode On"; +"Story.ToastStealthModeActivatedText" = "The creators of stories you viewed in the last **%1$@** or will view in the next **%2$@** won’t see you in the viewers’ lists."; + +"Story.ViewList.PremiumUpgradeText" = "List of viewers isn't available after 24 hours of story expiration.\n\nTo unlock viewers' lists for expired and saved stories, subscribe to [Telegram Premium]()."; +"Story.ViewList.PremiumUpgradeAction" = "Learn More"; +"Story.ViewList.PremiumUpgradeInlineText" = "To unlock viewers' lists for expired and saved stories, subscribe to [Telegram Premium]()."; +"Story.ViewList.NotFullyRecorded" = "Information about the other viewers wasn’t recorded."; +"Story.ViewList.EmptyTextSearch" = "No views found"; +"Story.ViewList.EmptyTextContacts" = "None of your contacts viewed this story."; +"Story.ViewList.ContextSortReactions" = "Reactions First"; +"Story.ViewList.ContextSortRecent" = "Recent First"; +"Story.ViewList.ContextSortInfo" = "Choose the order for the list of viewers."; +"Story.ViewList.TabTitleAll" = "All Viewers"; +"Story.ViewList.TabTitleContacts" = "Contacts"; +"Story.ViewList.TitleViewers" = "Viewers"; +"Story.ViewList.TitleEmpty" = "No Views"; +"Story.Footer.NoViews" = "No Views"; +"Story.Footer.ViewCount_1" = "|%d| View"; +"Story.Footer.ViewCount_any" = "|%d| Views"; +"Story.StealthMode.Title" = "Stealth Mode"; +"Story.StealthMode.ControlText" = "Turn Stealth Mode on to hide the fact that you viewed peoples' stories from them."; +"Story.StealthMode.UpgradeText" = "Subscribe to Telegram Premium to hide the fact that you viewed peoples' stories from them."; +"Story.StealthMode.RecentTitle" = "Hide Recent Views"; +"Story.StealthMode.RecentText" = "Hide my views in the last **%@**."; +"Story.StealthMode.NextTitle" = "Hide Next Views"; +"Story.StealthMode.NextText" = "Hide my views in the next **%@**."; +"Story.StealthMode.ToastCooldownText" = "Please wait until the **Stealth Mode** is ready to use again"; +"Story.StealthMode.EnableAction" = "Enable Stealth Mode"; +"Story.StealthMode.CooldownAction" = "Available in %@"; +"Story.StealthMode.UpgradeAction" = "Unlock Stealth Mode"; + +"Story.ViewLocation" = "View Location"; + +"Location.AddThisLocation" = "Add This Location"; +"Location.AddMyLocation" = "Add My Current Location"; +"Location.TypeCity" = "City"; +"Location.TypeStreet" = "Street"; +"Location.TypeLocation" = "Location"; diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index b87c7bab5d..cf91a69342 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -902,7 +902,7 @@ public protocol SharedAccountContext: AnyObject { func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> 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) -> 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 makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController diff --git a/submodules/ContextUI/Sources/ContextActionNode.swift b/submodules/ContextUI/Sources/ContextActionNode.swift index af50961aee..83918597a5 100644 --- a/submodules/ContextUI/Sources/ContextActionNode.swift +++ b/submodules/ContextUI/Sources/ContextActionNode.swift @@ -131,7 +131,11 @@ public final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol { self.iconNode.displaysAsynchronously = false self.iconNode.displayWithoutProcessing = true self.iconNode.isUserInteractionEnabled = false - if action.iconSource == nil { + if let iconSource = action.iconSource { + self.iconNode.clipsToBounds = true + self.iconNode.contentMode = iconSource.contentMode + self.iconNode.cornerRadius = iconSource.cornerRadius + } else { self.iconNode.image = action.icon(presentationData.theme) } diff --git a/submodules/ContextUI/Sources/ContextController.swift b/submodules/ContextUI/Sources/ContextController.swift index f13352629f..f1e00f7752 100644 --- a/submodules/ContextUI/Sources/ContextController.swift +++ b/submodules/ContextUI/Sources/ContextController.swift @@ -60,10 +60,14 @@ public enum ContextMenuActionItemFont { public struct ContextMenuActionItemIconSource { public let size: CGSize + public let contentMode: UIView.ContentMode + public let cornerRadius: CGFloat public let signal: Signal - public init(size: CGSize, signal: Signal) { + public init(size: CGSize, contentMode: UIView.ContentMode = .scaleToFill, cornerRadius: CGFloat = 0.0, signal: Signal) { self.size = size + self.contentMode = contentMode + self.cornerRadius = cornerRadius self.signal = signal } } diff --git a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift index 90b14f7ad4..413322b028 100644 --- a/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift +++ b/submodules/ContextUI/Sources/ContextControllerActionsStackNode.swift @@ -287,6 +287,9 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin let iconSize: CGSize? if let iconSource = self.item.iconSource { iconSize = iconSource.size + self.iconNode.cornerRadius = iconSource.cornerRadius + self.iconNode.contentMode = iconSource.contentMode + self.iconNode.clipsToBounds = true if self.iconDisposable == nil { self.iconDisposable = (iconSource.signal |> deliverOnMainQueue).start(next: { [weak self] image in guard let strongSelf = self else { diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 9558131325..fb602a37bd 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -460,7 +460,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { } func duplicate(_ entity: DrawingEntity) -> DrawingEntity { - let newEntity = entity.duplicate() + let newEntity = entity.duplicate(copy: false) self.prepareNewEntity(newEntity, setup: false, relativeTo: entity) guard let view = makeEntityView(context: self.context, entity: newEntity) else { diff --git a/submodules/DrawingUI/Sources/DrawingLocationEntity.swift b/submodules/DrawingUI/Sources/DrawingLocationEntity.swift index ba245c6303..7fa2c8e280 100644 --- a/submodules/DrawingUI/Sources/DrawingLocationEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingLocationEntity.swift @@ -171,6 +171,12 @@ public final class DrawingLocationEntityView: DrawingEntityView, UITextViewDeleg case .black: updatedStyle = .transparent case .transparent: + if self.locationEntity.hasCustomColor { + updatedStyle = .custom + } else { + updatedStyle = .white + } + case .custom: updatedStyle = .white case .blur: updatedStyle = .white @@ -217,6 +223,13 @@ public final class DrawingLocationEntityView: DrawingEntityView, UITextViewDeleg textColor = .black case .black, .transparent, .blur: textColor = .white + case .custom: + let color = self.locationEntity.color.toUIColor() + if color.lightness > 0.705 { + textColor = .black + } else { + textColor = .white + } } text.addAttribute(.foregroundColor, value: textColor, range: range) @@ -247,6 +260,18 @@ public final class DrawingLocationEntityView: DrawingEntityView, UITextViewDeleg self.backgroundView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.2) self.backgroundView.isHidden = false self.blurredBackgroundView.isHidden = true + case .custom: + let color = self.locationEntity.color.toUIColor() + let textColor: UIColor + if color.lightness > 0.705 { + textColor = .black + } else { + textColor = .white + } + self.textView.textColor = textColor + self.backgroundView.backgroundColor = color + self.backgroundView.isHidden = false + self.blurredBackgroundView.isHidden = true case .blur: self.textView.textColor = .white self.backgroundView.isHidden = true diff --git a/submodules/DrawingUI/Sources/DrawingTextEntity.swift b/submodules/DrawingUI/Sources/DrawingTextEntity.swift index f3a5d2d8a5..e59d49b8cc 100644 --- a/submodules/DrawingUI/Sources/DrawingTextEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingTextEntity.swift @@ -210,7 +210,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate func beginEditing(accessoryView: UIView?) { self._isEditing = true if !self.textEntity.text.string.isEmpty { - let previousEntity = self.textEntity.duplicate() as? DrawingTextEntity + let previousEntity = self.textEntity.duplicate(copy: false) as? DrawingTextEntity previousEntity?.uuid = self.textEntity.uuid self.previousEntity = previousEntity } diff --git a/submodules/DrawingUI/Sources/StickerPickerScreen.swift b/submodules/DrawingUI/Sources/StickerPickerScreen.swift index fb6e2dbd6c..a8f2936c65 100644 --- a/submodules/DrawingUI/Sources/StickerPickerScreen.swift +++ b/submodules/DrawingUI/Sources/StickerPickerScreen.swift @@ -2049,7 +2049,7 @@ final class StoryStickersContentView: UIView, EmojiCustomContentView { self.locationAction() } - func update(theme: PresentationTheme, useOpaqueTheme: Bool, availableSize: CGSize, transition: Transition) -> CGSize { + func update(theme: PresentationTheme, strings: PresentationStrings, useOpaqueTheme: Bool, availableSize: CGSize, transition: Transition) -> CGSize { if useOpaqueTheme { self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlOpaqueSelectionColor.cgColor self.tintBackgroundLayer.backgroundColor = UIColor.white.cgColor @@ -2065,7 +2065,7 @@ final class StoryStickersContentView: UIView, EmojiCustomContentView { let titleSize = self.title.update( transition: .immediate, component: AnyComponent(Text( - text: "ADD LOCATION", + text: strings.MediaEditor_AddLocation.uppercased(), font: Font.with(size: 23.0, design: .camera), color: .white )), diff --git a/submodules/Geocoding/Sources/Geocoding.swift b/submodules/Geocoding/Sources/Geocoding.swift index c0745657f3..c2ca6f0ba5 100644 --- a/submodules/Geocoding/Sources/Geocoding.swift +++ b/submodules/Geocoding/Sources/Geocoding.swift @@ -1,6 +1,7 @@ import Foundation import Contacts import CoreLocation +import MapKit import SwiftSignalKit public func geocodeLocation(address: String, locale: Locale? = nil) -> Signal<[CLPlacemark]?, NoError> { @@ -69,19 +70,47 @@ public struct ReverseGeocodedPlacemark { } } + +private let regions = [ + ( + CLLocationCoordinate2D(latitude: 46.046331, longitude: 32.398307), + CLLocationCoordinate2D(latitude: 44.326515, longitude: 36.613495) + ) +] + +private func shouldDisplayActualCountryName(latitude: Double, longitude: Double) -> Bool { + let coordinate = CLLocationCoordinate2D(latitude: latitude, longitude: longitude) + let point = MKMapPoint(coordinate) + for region in regions { + let p1 = MKMapPoint(region.0) + let p2 = MKMapPoint(region.1) + let rect = MKMapRect(x: min(p1.x, p2.x), y: min(p1.y, p2.y), width: abs(p1.x - p2.x), height: abs(p1.y - p2.y)) + if rect.contains(point) { + return false + } + } + return true +} + public func reverseGeocodeLocation(latitude: Double, longitude: Double, locale: Locale? = nil) -> Signal { return Signal { subscriber in let geocoder = CLGeocoder() geocoder.reverseGeocodeLocation(CLLocation(latitude: latitude, longitude: longitude), preferredLocale: locale, completionHandler: { placemarks, _ in - if let placemarks = placemarks, let placemark = placemarks.first { + if let placemarks, let placemark = placemarks.first { + var countryName = placemark.country + var countryCode = placemark.isoCountryCode + if !shouldDisplayActualCountryName(latitude: latitude, longitude: longitude) { + countryName = nil + countryCode = nil + } let result: ReverseGeocodedPlacemark if placemark.thoroughfare == nil && placemark.locality == nil && placemark.country == nil { result = ReverseGeocodedPlacemark(name: placemark.name, street: placemark.name, city: nil, country: nil, countryCode: nil) } else { if placemark.thoroughfare == nil && placemark.locality == nil, let ocean = placemark.ocean { - result = ReverseGeocodedPlacemark(name: ocean, street: nil, city: nil, country: placemark.country, countryCode: placemark.isoCountryCode) + result = ReverseGeocodedPlacemark(name: ocean, street: nil, city: nil, country: countryName, countryCode: countryCode) } else { - result = ReverseGeocodedPlacemark(name: nil, street: placemark.thoroughfare, city: placemark.locality, country: placemark.country, countryCode: placemark.isoCountryCode) + result = ReverseGeocodedPlacemark(name: nil, street: placemark.thoroughfare, city: placemark.locality, country: countryName, countryCode: countryCode) } } subscriber.putNext(result) diff --git a/submodules/LocationResources/Sources/VenueIconResources.swift b/submodules/LocationResources/Sources/VenueIconResources.swift index 2577be7779..09d0dc0948 100644 --- a/submodules/LocationResources/Sources/VenueIconResources.swift +++ b/submodules/LocationResources/Sources/VenueIconResources.swift @@ -134,8 +134,8 @@ public struct VenueIconArguments: TransformImageCustomArguments { } } -public func venueIcon(engine: TelegramEngine, type: String, background: Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - let isBuiltinIcon = ["", "home", "work"].contains(type) +public func venueIcon(engine: TelegramEngine, type: String, flag: String? = nil, background: Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + let isBuiltinIcon = ["", "home", "work"].contains(type) || flag != nil let data: Signal = isBuiltinIcon ? .single(nil) : venueIconData(engine: engine, resource: VenueIconResource(type: type)) return data |> map { data in return { arguments in @@ -164,7 +164,18 @@ public func venueIcon(engine: TelegramEngine, type: String, background: Bool) -> c.fillEllipse(in: CGRect(origin: CGPoint(), size: arguments.drawingRect.size)) } let boundsSize = CGSize(width: arguments.drawingRect.size.width - 4.0 * 2.0, height: arguments.drawingRect.size.height - 4.0 * 2.0) - if let image = iconImage, let cgImage = generateTintedImage(image: image, color: foregroundColor)?.cgImage { + if let flag { + let attributedString = NSAttributedString(string: flag, attributes: [NSAttributedString.Key.font: Font.regular(22.0), NSAttributedString.Key.foregroundColor: UIColor.white]) + + let line = CTLineCreateWithAttributedString(attributedString) + let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) + + let bounds = CGRect(origin: .zero, size: boundsSize) + let lineOrigin = CGPoint(x: floorToScreenPixels((bounds.size.width - lineBounds.size.width) / 2.0), y: floorToScreenPixels((bounds.size.height - lineBounds.size.height) / 2.0)) + + c.translateBy(x: lineOrigin.x + 3.0, y: lineOrigin.y + 7.0) + CTLineDraw(line, c) + } else if let image = iconImage, let cgImage = generateTintedImage(image: image, color: foregroundColor)?.cgImage { let fittedSize = image.size.aspectFitted(boundsSize) c.draw(cgImage, in: CGRect(origin: CGPoint(x: floor((arguments.drawingRect.width - fittedSize.width) / 2.0), y: floor((arguments.drawingRect.height - fittedSize.height) / 2.0)), size: fittedSize)) } else if isBuiltinIcon { diff --git a/submodules/LocationUI/Sources/LocationActionListItem.swift b/submodules/LocationUI/Sources/LocationActionListItem.swift index a55ab08c8e..9a838c35d8 100644 --- a/submodules/LocationUI/Sources/LocationActionListItem.swift +++ b/submodules/LocationUI/Sources/LocationActionListItem.swift @@ -268,22 +268,36 @@ final class LocationActionListItemNode: ListViewItemNode { var arguments: TransformImageCustomArguments? if let updatedIcon = updatedIcon { switch updatedIcon { - case .location: - strongSelf.iconNode.isHidden = false - strongSelf.venueIconNode.isHidden = true - strongSelf.iconNode.image = generateLocationIcon(theme: item.presentationData.theme) - case .liveLocation, .stopLiveLocation: - strongSelf.iconNode.isHidden = false - strongSelf.venueIconNode.isHidden = true - strongSelf.iconNode.image = generateLiveLocationIcon(theme: item.presentationData.theme, stop: updatedIcon == .stopLiveLocation) - case let .venue(venue): - strongSelf.iconNode.isHidden = true - strongSelf.venueIconNode.isHidden = false - strongSelf.venueIconNode.setSignal(venueIcon(engine: item.engine, type: venue.venue?.type ?? "", background: true)) - - if venue.venue?.id == "city" { - arguments = VenueIconArguments(defaultBackgroundColor: item.presentationData.theme.chat.inputPanel.actionControlFillColor, defaultForegroundColor: .white) + case .location: + strongSelf.iconNode.isHidden = false + strongSelf.venueIconNode.isHidden = true + strongSelf.iconNode.image = generateLocationIcon(theme: item.presentationData.theme) + case .liveLocation, .stopLiveLocation: + strongSelf.iconNode.isHidden = false + strongSelf.venueIconNode.isHidden = true + strongSelf.iconNode.image = generateLiveLocationIcon(theme: item.presentationData.theme, stop: updatedIcon == .stopLiveLocation) + case let .venue(venue): + strongSelf.iconNode.isHidden = true + strongSelf.venueIconNode.isHidden = false + + func flagEmoji(countryCode: String) -> String { + let base : UInt32 = 127397 + var flagString = "" + for v in countryCode.uppercased().unicodeScalars { + flagString.unicodeScalars.append(UnicodeScalar(base + v.value)!) } + return flagString + } + let type = venue.venue?.type + var flag: String? + if let venue = venue.venue, venue.provider == "city", let countryCode = venue.id { + flag = flagEmoji(countryCode: countryCode) + } + + if venue.venue?.provider == "city" { + arguments = VenueIconArguments(defaultBackgroundColor: item.presentationData.theme.chat.inputPanel.actionControlFillColor, defaultForegroundColor: .white) + } + strongSelf.venueIconNode.setSignal(venueIcon(engine: item.engine, type: type ?? "", flag: flag, background: true)) } if updatedIcon == .stopLiveLocation { diff --git a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift index 47a8545da5..7b2a393180 100644 --- a/submodules/LocationUI/Sources/LocationPickerControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationPickerControllerNode.swift @@ -150,7 +150,7 @@ private enum LocationPickerEntry: Comparable, Identifiable { case let .city(_, title, subtitle, _, _, _, coordinate, name, countryCode): let icon: LocationActionListItemIcon if let name { - icon = .venue(TelegramMediaMap(latitude: 0, longitude: 0, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: MapVenue(title: name, address: "City", provider: nil, id: "city", type: "building/default"), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) + icon = .venue(TelegramMediaMap(latitude: 0, longitude: 0, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: MapVenue(title: name, address: presentationData.strings.Location_TypeCity, provider: "city", id: countryCode, type: "building/default"), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) } else { icon = .location } @@ -583,20 +583,28 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM switch strongSelf.mode { case .share: if source == .story { - title = "Add This Location" + title = presentationData.strings.Location_AddThisLocation } else { title = presentationData.strings.Map_SendThisLocation } case .pick: title = presentationData.strings.Map_SetThisLocation } - entries.append(.location(presentationData.theme, title, address ?? presentationData.strings.Map_Locating, nil, nil, nil, coordinate, state.street, state.countryCode, true)) + if source == .story { + if state.street != "" { + entries.append(.location(presentationData.theme, state.street ?? presentationData.strings.Map_Locating, state.isStreet ? presentationData.strings.Location_TypeStreet : presentationData.strings.Location_TypeLocation, nil, nil, nil, coordinate, state.street, nil, false)) + } else if state.city != "" { + entries.append(.city(presentationData.theme, state.city ?? presentationData.strings.Map_Locating, presentationData.strings.Location_TypeCity, nil, nil, nil, coordinate, state.city, state.countryCode)) + } + } else { + entries.append(.location(presentationData.theme, title, address ?? presentationData.strings.Map_Locating, nil, nil, nil, coordinate, state.street, nil, true)) + } case .selecting: let title: String switch strongSelf.mode { case .share: if source == .story { - title = "Add This Location" + title = presentationData.strings.Location_AddThisLocation } else { title = presentationData.strings.Map_SendThisLocation } @@ -620,10 +628,10 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM case .share: if source == .story { if let initialLocation = strongSelf.controller?.initialLocation { - title = "Add This Location" + title = presentationData.strings.Location_AddThisLocation coordinate = initialLocation } else { - title = "Add My Current Location" + title = presentationData.strings.Location_AddMyLocation } } else { title = presentationData.strings.Map_SendMyCurrentLocation @@ -633,10 +641,10 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM } if source == .story { if state.city != "" { - entries.append(.city(presentationData.theme, state.city ?? presentationData.strings.Map_Locating, "City", nil, nil, nil, coordinate, state.city, state.countryCode)) + entries.append(.city(presentationData.theme, state.city ?? presentationData.strings.Map_Locating, presentationData.strings.Location_TypeCity, nil, nil, nil, coordinate, state.city, state.countryCode)) } if state.street != "" { - entries.append(.location(presentationData.theme, state.street ?? presentationData.strings.Map_Locating, state.isStreet ? "Street" : "Location", nil, nil, nil, coordinate, state.street, nil, false)) + entries.append(.location(presentationData.theme, state.street ?? presentationData.strings.Map_Locating, state.isStreet ? presentationData.strings.Location_TypeStreet : presentationData.strings.Location_TypeLocation, nil, nil, nil, coordinate, state.street, nil, false)) } } else { entries.append(.location(presentationData.theme, title, (userLocation?.horizontalAccuracy).flatMap { presentationData.strings.Map_AccurateTo(stringForDistance(strings: presentationData.strings, distance: $0)).string } ?? presentationData.strings.Map_Locating, nil, nil, nil, coordinate, state.street, nil, true)) @@ -790,8 +798,12 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM var cityName: String? var streetName: String? let countryCode = placemark?.countryCode - if let city = placemark?.city, let countryCode = placemark?.countryCode { - cityName = "\(city), \(displayCountryName(countryCode, locale: locale))" + if let city = placemark?.city { + if let countryCode = placemark?.countryCode { + cityName = "\(city), \(displayCountryName(countryCode, locale: locale))" + } else { + cityName = city + } } else { cityName = "" } @@ -809,7 +821,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM streetName = "" } if streetName == "" && cityName == "" { - streetName = "Location" + streetName = presentationData.strings.Location_TypeLocation } strongSelf.updateState { state in var state = state @@ -835,8 +847,12 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM var cityName: String? var streetName: String? let countryCode = placemark?.countryCode - if let city = placemark?.city, let countryCode = placemark?.countryCode { - cityName = "\(city), \(displayCountryName(countryCode, locale: locale))" + if let city = placemark?.city { + if let countryCode = placemark?.countryCode { + cityName = "\(city), \(displayCountryName(countryCode, locale: locale))" + } else { + cityName = city + } } else { cityName = "" } @@ -854,7 +870,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM streetName = "" } if streetName == "" && cityName == "" { - streetName = "Location" + streetName = presentationData.strings.Location_TypeLocation } strongSelf.updateState { state in var state = state diff --git a/submodules/LocationUI/Sources/LocationViewControllerNode.swift b/submodules/LocationUI/Sources/LocationViewControllerNode.swift index 578cd30ff3..9f13a79efd 100644 --- a/submodules/LocationUI/Sources/LocationViewControllerNode.swift +++ b/submodules/LocationUI/Sources/LocationViewControllerNode.swift @@ -291,6 +291,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan var eta: Signal<(ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime), NoError> = .single((.calculating, .calculating, .calculating)) var address: Signal = .single(nil) + let locale = localeWithStrings(presentationData.strings) if let location = getLocation(from: subject), location.liveBroadcastingTimeout == nil { eta = .single((.calculating, .calculating, .calculating)) |> then(combineLatest(queue: Queue.mainQueue(), getExpectedTravelTime(coordinate: location.coordinate, transportType: .automobile), getExpectedTravelTime(coordinate: location.coordinate, transportType: .transit), getExpectedTravelTime(coordinate: location.coordinate, transportType: .walking)) @@ -313,7 +314,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan } else { address = .single(nil) |> then( - reverseGeocodeLocation(latitude: location.latitude, longitude: location.longitude) + reverseGeocodeLocation(latitude: location.latitude, longitude: location.longitude, locale: locale) |> map { placemark -> String? in return placemark?.compactDisplayAddress ?? "" } diff --git a/submodules/MediaPickerUI/Sources/MediaGroupsContextMenuContent.swift b/submodules/MediaPickerUI/Sources/MediaGroupsContextMenuContent.swift new file mode 100644 index 0000000000..25838debc9 --- /dev/null +++ b/submodules/MediaPickerUI/Sources/MediaGroupsContextMenuContent.swift @@ -0,0 +1,438 @@ +import Foundation +import UIKit +import AsyncDisplayKit +import Display +import ContextUI +import AccountContext +import TelegramPresentationData +import Photos + +struct MediaGroupItem { + let collection: PHAssetCollection + let firstItem: PHAsset? + let count: Int +} + +final class MediaGroupsContextMenuContent: ContextControllerItemsContent { + private final class GroupsListNode: ASDisplayNode, UIScrollViewDelegate { + private final class ItemNode: HighlightTrackingButtonNode { + let context: AccountContext + let highlightBackgroundNode: ASDisplayNode + let titleLabelNode: ImmediateTextNode + let subtitleLabelNode: ImmediateTextNode + let iconNode: ImageNode + let separatorNode: ASDisplayNode + + let action: () -> Void + + private var item: MediaGroupItem? + + init(context: AccountContext, action: @escaping () -> Void) { + self.action = action + self.context = context + + self.highlightBackgroundNode = ASDisplayNode() + self.highlightBackgroundNode.isAccessibilityElement = false + self.highlightBackgroundNode.alpha = 0.0 + + self.titleLabelNode = ImmediateTextNode() + self.titleLabelNode.isAccessibilityElement = false + self.titleLabelNode.maximumNumberOfLines = 1 + self.titleLabelNode.isUserInteractionEnabled = false + + self.subtitleLabelNode = ImmediateTextNode() + self.subtitleLabelNode.isAccessibilityElement = false + self.subtitleLabelNode.maximumNumberOfLines = 1 + self.subtitleLabelNode.isUserInteractionEnabled = false + + self.iconNode = ImageNode() + self.iconNode.clipsToBounds = true + self.iconNode.contentMode = .scaleAspectFill + self.iconNode.cornerRadius = 6.0 + + self.separatorNode = ASDisplayNode() + self.separatorNode.isAccessibilityElement = false + + super.init() + + self.isAccessibilityElement = true + + self.addSubnode(self.separatorNode) + self.addSubnode(self.highlightBackgroundNode) + self.addSubnode(self.titleLabelNode) + self.addSubnode(self.subtitleLabelNode) + self.addSubnode(self.iconNode) + + self.highligthedChanged = { [weak self] highlighted in + guard let strongSelf = self else { + return + } + if highlighted { + strongSelf.highlightBackgroundNode.alpha = 1.0 + } else { + let previousAlpha = strongSelf.highlightBackgroundNode.alpha + strongSelf.highlightBackgroundNode.alpha = 0.0 + strongSelf.highlightBackgroundNode.layer.animateAlpha(from: previousAlpha, to: 0.0, duration: 0.2) + } + } + + self.addTarget(self, action: #selector(self.pressed), forControlEvents: .touchUpInside) + } + + @objc private func pressed() { + self.action() + } + + func update(size: CGSize, presentationData: PresentationData, item: MediaGroupItem, isLast: Bool, syncronousLoad: Bool) { + let leftInset: CGFloat = 16.0 + let rightInset: CGFloat = 48.0 + + if self.item?.collection.localIdentifier != item.collection.localIdentifier { + self.item = item + + self.accessibilityLabel = item.collection.localizedTitle + + if let asset = item.firstItem { + self.iconNode.setSignal(assetImage(asset: asset, targetSize: CGSize(width: 24.0, height: 24.0), exact: false)) + } + } + + self.highlightBackgroundNode.backgroundColor = presentationData.theme.contextMenu.itemHighlightedBackgroundColor + + self.highlightBackgroundNode.frame = CGRect(origin: CGPoint(), size: size) + + self.titleLabelNode.attributedText = NSAttributedString(string: item.collection.localizedTitle ?? "", font: Font.regular(17.0), textColor: presentationData.theme.contextMenu.primaryColor) + + self.subtitleLabelNode.attributedText = NSAttributedString(string: "\(item.count)", font: Font.regular(15.0), textColor: presentationData.theme.contextMenu.secondaryColor) + let maxTextWidth: CGFloat = size.width - leftInset - rightInset + + let titleSize = self.titleLabelNode.updateLayout(CGSize(width: maxTextWidth, height: 100.0)) + let subtitleSize = self.subtitleLabelNode.updateLayout(CGSize(width: maxTextWidth, height: 100.0)) + + let spacing: CGFloat = 2.0 + let contentHeight = titleSize.height + spacing + subtitleSize.height + + let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((size.height - contentHeight) / 2.0)), size: titleSize) + self.titleLabelNode.frame = titleFrame + + let subtitleFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + spacing), size: titleSize) + self.subtitleLabelNode.frame = subtitleFrame + + let iconSize = CGSize(width: 24.0, height: 24.0) + let iconFrame = CGRect(origin: CGPoint(x: size.width - leftInset - iconSize.width, y: floor((size.height - iconSize.height) / 2.0)), size: iconSize) + self.iconNode.frame = iconFrame + + self.separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor + self.separatorNode.frame = CGRect(origin: CGPoint(x: 0.0, y: size.height), size: CGSize(width: size.width, height: UIScreenPixel)) + self.separatorNode.isHidden = isLast + } + } + + private let context: AccountContext + private let items: [MediaGroupItem] + private let requestUpdate: (GroupsListNode, ContainedViewLayoutTransition) -> Void + private let requestUpdateApparentHeight: (GroupsListNode, ContainedViewLayoutTransition) -> Void + private let selectGroup: (PHAssetCollection) -> Void + + private let scrollNode: ASScrollNode + private var ignoreScrolling: Bool = false + private var animateIn: Bool = false + private var bottomScrollInset: CGFloat = 0.0 + + private var presentationData: PresentationData? + private var currentSize: CGSize? + private var apparentHeight: CGFloat = 0.0 + + private var itemNodes: [Int: ItemNode] = [:] + + init( + context: AccountContext, + items: [MediaGroupItem], + requestUpdate: @escaping (GroupsListNode, ContainedViewLayoutTransition) -> Void, + requestUpdateApparentHeight: @escaping (GroupsListNode, ContainedViewLayoutTransition) -> Void, + selectGroup: @escaping (PHAssetCollection) -> Void + ) { + self.context = context + self.items = items + self.requestUpdate = requestUpdate + self.requestUpdateApparentHeight = requestUpdateApparentHeight + self.selectGroup = selectGroup + + self.scrollNode = ASScrollNode() + self.scrollNode.canCancelAllTouchesInViews = true + self.scrollNode.view.delaysContentTouches = false + self.scrollNode.view.showsVerticalScrollIndicator = false + if #available(iOS 11.0, *) { + self.scrollNode.view.contentInsetAdjustmentBehavior = .never + } + self.scrollNode.clipsToBounds = false + + super.init() + + self.addSubnode(self.scrollNode) + self.scrollNode.view.delegate = self + + self.clipsToBounds = true + } + + func scrollViewDidScroll(_ scrollView: UIScrollView) { + if self.ignoreScrolling { + return + } + self.updateVisibleItems(animated: false, syncronousLoad: false) + + if let size = self.currentSize { + var apparentHeight = -self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentSize.height + apparentHeight = max(apparentHeight, 44.0) + apparentHeight = min(apparentHeight, size.height) + if self.apparentHeight != apparentHeight { + self.apparentHeight = apparentHeight + + self.requestUpdateApparentHeight(self, .immediate) + } + } + } + + private func updateVisibleItems(animated: Bool, syncronousLoad: Bool) { + guard let size = self.currentSize else { + return + } + guard let presentationData = self.presentationData else { + return + } + let itemHeight: CGFloat = 54.0 + let visibleBounds = self.scrollNode.bounds.insetBy(dx: 0.0, dy: -180.0) + + var validIds = Set() + + let minVisibleIndex = max(0, Int(floor(visibleBounds.minY / itemHeight))) + let maxVisibleIndex = Int(ceil(visibleBounds.maxY / itemHeight)) + + if minVisibleIndex <= maxVisibleIndex { + for index in minVisibleIndex ... maxVisibleIndex { + if index < self.items.count { + let height = itemHeight + let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: CGFloat(index) * itemHeight), size: CGSize(width: size.width, height: height)) + + let item = self.items[index] + validIds.insert(index) + + let itemNode: ItemNode + if let current = self.itemNodes[index] { + itemNode = current + } else { + let selectGroup = self.selectGroup + itemNode = ItemNode(context: self.context, action: { + selectGroup(item.collection) + }) + self.itemNodes[index] = itemNode + self.scrollNode.addSubnode(itemNode) + } + + itemNode.update(size: itemFrame.size, presentationData: presentationData, item: item, isLast: index == self.items.count - 1, syncronousLoad: syncronousLoad) + itemNode.frame = itemFrame + } + } + } + + var removeIds: [Int] = [] + for (id, itemNode) in self.itemNodes { + if !validIds.contains(id) { + removeIds.append(id) + itemNode.removeFromSupernode() + } + } + for id in removeIds { + self.itemNodes.removeValue(forKey: id) + } + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + var extendedScrollNodeFrame = self.scrollNode.frame + extendedScrollNodeFrame.size.height += self.bottomScrollInset + + if extendedScrollNodeFrame.contains(point) { + return self.scrollNode.view.hitTest(self.view.convert(point, to: self.scrollNode.view), with: event) + } + + return super.hitTest(point, with: event) + } + + func update(presentationData: PresentationData, constrainedSize: CGSize, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (height: CGFloat, apparentHeight: CGFloat) { + let itemHeight: CGFloat = 54.0 + + self.presentationData = presentationData + + let contentHeight = CGFloat(self.items.count) * itemHeight + let size = CGSize(width: constrainedSize.width, height: contentHeight) + + let containerSize = CGSize(width: size.width, height: min(constrainedSize.height, size.height)) + self.currentSize = containerSize + + self.ignoreScrolling = true + + if self.scrollNode.frame != CGRect(origin: CGPoint(), size: containerSize) { + self.scrollNode.frame = CGRect(origin: CGPoint(), size: containerSize) + } + if self.scrollNode.view.contentInset.bottom != bottomInset { + self.scrollNode.view.contentInset.bottom = bottomInset + } + self.bottomScrollInset = bottomInset + let scrollContentSize = CGSize(width: size.width, height: size.height) + if self.scrollNode.view.contentSize != scrollContentSize { + self.scrollNode.view.contentSize = scrollContentSize + } + self.ignoreScrolling = false + + self.updateVisibleItems(animated: transition.isAnimated, syncronousLoad: !transition.isAnimated) + + self.animateIn = false + + var apparentHeight = -self.scrollNode.view.contentOffset.y + self.scrollNode.view.contentSize.height + apparentHeight = max(apparentHeight, 44.0) + apparentHeight = min(apparentHeight, containerSize.height) + self.apparentHeight = apparentHeight + + return (containerSize.height, apparentHeight) + } + } + + final class ItemsNode: ASDisplayNode, ContextControllerItemsNode { + private let context: AccountContext + private let items: [MediaGroupItem] + private let requestUpdate: (ContainedViewLayoutTransition) -> Void + private let requestUpdateApparentHeight: (ContainedViewLayoutTransition) -> Void + + private var presentationData: PresentationData + + private let currentTabIndex: Int = 0 + private var visibleTabNodes: [Int: GroupsListNode] = [:] + + private let selectGroup: (PHAssetCollection) -> Void + + private(set) var apparentHeight: CGFloat = 0.0 + + init( + context: AccountContext, + items: [MediaGroupItem], + requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void, + requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void, + selectGroup: @escaping (PHAssetCollection) -> Void + ) { + self.context = context + self.items = items + self.selectGroup = selectGroup + self.presentationData = context.sharedContext.currentPresentationData.with({ $0 }) + + self.requestUpdate = requestUpdate + self.requestUpdateApparentHeight = requestUpdateApparentHeight + + super.init() + } + + func update(presentationData: PresentationData, constrainedWidth: CGFloat, maxHeight: CGFloat, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) -> (cleanSize: CGSize, apparentHeight: CGFloat) { + let constrainedSize = CGSize(width: min(190.0, constrainedWidth), height: min(295.0, maxHeight)) + + let topContentHeight: CGFloat = 0.0 + + var tabLayouts: [Int: (height: CGFloat, apparentHeight: CGFloat)] = [:] + + var visibleIndices: [Int] = [] + visibleIndices.append(self.currentTabIndex) + + let previousVisibleTabFrames: [(Int, CGRect)] = self.visibleTabNodes.map { key, value -> (Int, CGRect) in + return (key, value.frame) + } + + for index in visibleIndices { + var tabTransition = transition + let tabNode: GroupsListNode + var initialReferenceFrame: CGRect? + if let current = self.visibleTabNodes[index] { + tabNode = current + } else { + for (previousIndex, previousFrame) in previousVisibleTabFrames { + if index > previousIndex { + initialReferenceFrame = previousFrame.offsetBy(dx: constrainedSize.width, dy: 0.0) + } else { + initialReferenceFrame = previousFrame.offsetBy(dx: -constrainedSize.width, dy: 0.0) + } + break + } + + tabNode = GroupsListNode( + context: self.context, + items: self.items, + requestUpdate: { [weak self] tab, transition in + guard let strongSelf = self else { + return + } + if strongSelf.visibleTabNodes.contains(where: { $0.value === tab }) { + strongSelf.requestUpdate(transition) + } + }, + requestUpdateApparentHeight: { [weak self] tab, transition in + guard let strongSelf = self else { + return + } + if strongSelf.visibleTabNodes.contains(where: { $0.value === tab }) { + strongSelf.requestUpdateApparentHeight(transition) + } + }, + selectGroup: self.selectGroup + ) + self.addSubnode(tabNode) + self.visibleTabNodes[index] = tabNode + tabTransition = .immediate + } + + let tabLayout = tabNode.update(presentationData: presentationData, constrainedSize: CGSize(width: constrainedSize.width, height: constrainedSize.height - topContentHeight), bottomInset: bottomInset, transition: tabTransition) + tabLayouts[index] = tabLayout + let currentFractionalTabIndex = CGFloat(self.currentTabIndex) + let xOffset: CGFloat = (CGFloat(index) - currentFractionalTabIndex) * constrainedSize.width + let tabFrame = CGRect(origin: CGPoint(x: xOffset, y: topContentHeight), size: CGSize(width: constrainedSize.width, height: tabLayout.height)) + tabTransition.updateFrame(node: tabNode, frame: tabFrame) + if let initialReferenceFrame = initialReferenceFrame { + transition.animatePositionAdditive(node: tabNode, offset: CGPoint(x: initialReferenceFrame.minX - tabFrame.minX, y: 0.0)) + } + } + + var contentSize = CGSize(width: constrainedSize.width, height: topContentHeight) + var apparentHeight = topContentHeight + + if let tabLayout = tabLayouts[self.currentTabIndex] { + contentSize.height += tabLayout.height + apparentHeight += tabLayout.apparentHeight + } + + return (contentSize, apparentHeight) + } + } + + let context: AccountContext + let items: [MediaGroupItem] + let selectGroup: (PHAssetCollection) -> Void + + public init( + context: AccountContext, + items: [MediaGroupItem], + selectGroup: @escaping (PHAssetCollection) -> Void + ) { + self.context = context + self.items = items + self.selectGroup = selectGroup + } + + func node( + requestUpdate: @escaping (ContainedViewLayoutTransition) -> Void, + requestUpdateApparentHeight: @escaping (ContainedViewLayoutTransition) -> Void + ) -> ContextControllerItemsNode { + return ItemsNode( + context: self.context, + items: self.items, + requestUpdate: requestUpdate, + requestUpdateApparentHeight: requestUpdateApparentHeight, + selectGroup: self.selectGroup + ) + } +} diff --git a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift index f6b688fbbd..e947302ed5 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift @@ -309,6 +309,9 @@ final class MediaPickerGridItemNode: GridItemNode { self.progressDisposable.set(nil) self.updateProgress(nil, animated: false) + + self.backgroundNode.image = nil + self.imageNode.contentMode = .scaleAspectFill } if self.draftNode.supernode == nil { @@ -403,6 +406,7 @@ final class MediaPickerGridItemNode: GridItemNode { if asset.localIdentifier == self.currentAsset?.localIdentifier { return } + self.backgroundNode.image = nil self.progressDisposable.set( (interaction.downloadManager.downloadProgress(identifier: asset.localIdentifier) diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 635916808d..0b09329aa4 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -73,7 +73,7 @@ private struct MediaPickerGridTransaction { let scrollToItem: GridNodeScrollToItem? init(previousList: [MediaPickerGridEntry], list: [MediaPickerGridEntry], context: AccountContext, interaction: MediaPickerInteraction, theme: PresentationTheme, strings: PresentationStrings, scrollToItem: GridNodeScrollToItem?) { - let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: previousList, rightList: list) + let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: previousList, rightList: list) self.deletions = deleteIndices self.insertions = indicesAndItems.map { GridNodeInsertItem(index: $0.0, item: $0.1.item(context: context, interaction: interaction, theme: theme, strings: strings), previousIndex: $0.2) } @@ -81,6 +81,19 @@ private struct MediaPickerGridTransaction { self.scrollToItem = scrollToItem } + + init(clearList: [MediaPickerGridEntry]) { + var deletions: [Int] = [] + var i = 0 + for _ in clearList { + deletions.append(i) + i += 1 + } + self.deletions = deletions + self.insertions = [] + self.updates = [] + self.scrollToItem = nil + } } struct Month: Equatable { @@ -186,6 +199,8 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { public var isContainerPanning: () -> Bool = { return false } public var isContainerExpanded: () -> Bool = { return false } + private let selectedCollection = Promise(nil) + var dismissAll: () -> Void = { } private class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { @@ -237,7 +252,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { private var fastScrollContentOffset = ValuePromise(ignoreRepeated: true) private var fastScrollDisposable: Disposable? - + private var didSetReady = false private let _ready = Promise() var ready: Promise { @@ -284,6 +299,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.containerNode.addSubnode(self.gridNode) self.containerNode.addSubnode(self.scrollingArea) + let selectedCollection = controller.selectedCollection.get() let preloadPromise = self.preloadPromise let updatedState: Signal switch controller.subject { @@ -301,15 +317,19 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } else if [.restricted, .denied].contains(mediaAccess) { return .single(.noAccess(cameraAccess: cameraAccess)) } else { - if let collection = collection { - return combineLatest(mediaAssetsContext.fetchAssets(collection), preloadPromise.get()) - |> map { fetchResult, preload in - return .assets(fetchResult: fetchResult, preload: preload, drafts: [], mediaAccess: mediaAccess, cameraAccess: cameraAccess) - } - } else { - return combineLatest(mediaAssetsContext.recentAssets(), preloadPromise.get(), drafts) - |> map { fetchResult, preload, drafts in - return .assets(fetchResult: fetchResult, preload: preload, drafts: drafts, mediaAccess: mediaAccess, cameraAccess: cameraAccess) + return selectedCollection + |> mapToSignal { selectedCollection in + let collection = selectedCollection ?? collection + if let collection { + return combineLatest(mediaAssetsContext.fetchAssets(collection), preloadPromise.get()) + |> map { fetchResult, preload in + return .assets(fetchResult: fetchResult, preload: preload, drafts: [], mediaAccess: mediaAccess, cameraAccess: selectedCollection != nil ? nil : cameraAccess) + } + } else { + return combineLatest(mediaAssetsContext.recentAssets(), preloadPromise.get(), drafts) + |> map { fetchResult, preload, drafts in + return .assets(fetchResult: fetchResult, preload: preload, drafts: drafts, mediaAccess: mediaAccess, cameraAccess: cameraAccess) + } } } } @@ -578,6 +598,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { ) } + fileprivate var resetOnUpdate = false private func updateState(_ state: State) { guard let controller = self.controller, let interaction = controller.interaction else { return @@ -652,6 +673,72 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.requestedCameraAccess = true self.mediaAssetsContext.requestCameraAccess() } + + if !controller.didSetupGroups { + controller.didSetupGroups = true + controller.groupsPromise.set( + combineLatest( + self.mediaAssetsContext.fetchAssetsCollections(.album), + self.mediaAssetsContext.fetchAssetsCollections(.smartAlbum) + ) + |> map { albums, smartAlbums -> [MediaGroupItem] in + var collections: [PHAssetCollection] = [] + smartAlbums.enumerateObjects { collection, _, _ in + if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) { + collections.append(collection) + } + } + smartAlbums.enumerateObjects { collection, index, _ in + var supportedAlbums: [PHAssetCollectionSubtype] = [ + .smartAlbumBursts, + .smartAlbumPanoramas, + .smartAlbumScreenshots, + .smartAlbumSelfPortraits, + .smartAlbumSlomoVideos, + .smartAlbumTimelapses, + .smartAlbumVideos, + .smartAlbumAllHidden + ] + if #available(iOS 11, *) { + supportedAlbums.append(.smartAlbumAnimated) + supportedAlbums.append(.smartAlbumDepthEffect) + supportedAlbums.append(.smartAlbumLivePhotos) + } + if supportedAlbums.contains(collection.assetCollectionSubtype) { + let result = PHAsset.fetchAssets(in: collection, options: nil) + if result.count > 0 { + collections.append(collection) + } + } + } + albums.enumerateObjects(options: [.reverse]) { collection, _, _ in + let result = PHAsset.fetchAssets(in: collection, options: nil) + if result.count > 0 { + collections.append(collection) + } + } + + var items: [MediaGroupItem] = [] + for collection in collections { + let result = PHAsset.fetchAssets(in: collection, options: nil) + let firstItem: PHAsset? + if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) { + firstItem = result.lastObject + } else { + firstItem = result.firstObject + } + items.append( + MediaGroupItem( + collection: collection, + firstItem: firstItem, + count: result.count + ) + ) + } + return items + } + ) + } } else if case .notDetermined = mediaAccess, !self.requestedMediaAccess { self.requestedMediaAccess = true self.mediaAssetsContext.requestMediaAccess() @@ -664,7 +751,16 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } } - let previousEntries = self.currentEntries + + + var previousEntries = self.currentEntries + + if self.resetOnUpdate { + self.enqueueTransaction(MediaPickerGridTransaction(clearList: previousEntries)) + self.resetOnUpdate = false + previousEntries = [] + } + self.currentEntries = entries var scrollToItem: GridNodeScrollToItem? @@ -685,6 +781,10 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.updateNavigation(transition: .immediate) } + private func resetItems() { + + } + private func updateSelectionState(animated: Bool = false) { self.gridNode.forEachItemNode { itemNode in if let itemNode = itemNode as? MediaPickerGridItemNode { @@ -1455,8 +1555,12 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.titleView.title = collection.localizedTitle ?? presentationData.strings.Attachment_Gallery } else { switch mode { - case .default, .story: - self.titleView.title = presentationData.strings.Attachment_Gallery + case .default: + self.titleView.title = presentationData.strings.MediaPicker_Recents + self.titleView.isEnabled = true + case .story: + self.titleView.title = presentationData.strings.MediaPicker_Recents + self.titleView.isEnabled = true case .wallpaper: self.titleView.title = presentationData.strings.Conversation_Theme_ChooseWallpaperTitle case .addImage: @@ -1527,6 +1631,12 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { strongSelf.controllerNode.updateDisplayMode(index == 0 ? .all : .selected) } } + + self.titleView.action = { [weak self] in + if let self { + self.presentGroupsMenu() + } + } self.navigationItem.titleView = self.titleView @@ -1703,6 +1813,59 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { self.controllerNode.closeGalleryController() } + public var groupsPresented: () -> Void = {} + + private var didSetupGroups = false + private let groupsPromise = Promise<[MediaGroupItem]>() + + public func presentGroupsMenu() { + self.groupsPresented() + + let _ = (self.groupsPromise.get() + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] items in + guard let self else { + return + } + var dismissImpl: (() -> Void)? + let content: ContextControllerItemsContent = MediaGroupsContextMenuContent( + context: self.context, + items: items, + selectGroup: { [weak self] collection in + guard let self else { + return + } + self.controllerNode.resetOnUpdate = true + if collection.assetCollectionSubtype == .smartAlbumUserLibrary { + self.selectedCollection.set(.single(nil)) + self.titleView.title = self.presentationData.strings.MediaPicker_Recents + } else { + self.selectedCollection.set(.single(collection)) + self.titleView.title = collection.localizedTitle ?? "" + } + self.scrollToTop?() + dismissImpl?() + } + ) + + self.titleView.isHighlighted = true + let contextController = ContextController( + account: self.context.account, + presentationData: self.presentationData, + source: .reference(MediaPickerContextReferenceContentSource(controller: self, sourceNode: self.titleView.contextSourceNode)), + items: .single(ContextController.Items(content: .custom(content))), + gesture: nil + ) + contextController.dismissed = { [weak self] in + self?.titleView.isHighlighted = false + } + dismissImpl = { [weak contextController] in + contextController?.dismiss() + } + self.presentInGlobalOverlay(contextController) + }) + } + private weak var undoOverlayController: UndoOverlayController? private func showSelectionUndo(item: TGMediaSelectableItem) { let scale = min(2.0, UIScreenScale) @@ -2336,7 +2499,8 @@ public func storyMediaPickerController( context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, - dismissed: @escaping () -> Void + dismissed: @escaping () -> Void, + groupsPresented: @escaping () -> Void ) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme) let updatedPresentationData: (PresentationData, Signal) = (presentationData, .single(presentationData)) @@ -2347,6 +2511,7 @@ public func storyMediaPickerController( controller.getSourceRect = getSourceRect controller.requestController = { _, present in let mediaPickerController = MediaPickerScreen(context: context, updatedPresentationData: updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .story), mainButtonState: nil, mainButtonAction: nil) + mediaPickerController.groupsPresented = groupsPresented mediaPickerController.customSelection = { controller, result in if let result = result as? MediaEditorDraft { controller.updateHiddenMediaId(result.path) diff --git a/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift b/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift index 7fe0210eeb..eb42c6da28 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerTitleView.swift @@ -4,9 +4,13 @@ import AsyncDisplayKit import Display import TelegramPresentationData import SegmentedControlNode +import ContextUI final class MediaPickerTitleView: UIView { + let contextSourceNode: ContextReferenceContentNode + private let buttonNode: HighlightTrackingButtonNode private let titleNode: ImmediateTextNode + private let arrowNode: ASImageNode private let segmentedControlNode: SegmentedControlNode public var theme: PresentationTheme { @@ -25,11 +29,25 @@ final class MediaPickerTitleView: UIView { } } + public var isEnabled: Bool = false { + didSet { + self.buttonNode.isUserInteractionEnabled = self.isEnabled + self.arrowNode.isHidden = !self.isEnabled + } + } + + public var isHighlighted: Bool = false { + didSet { + self.alpha = self.isHighlighted ? 0.5 : 1.0 + } + } + public var segmentsHidden = true { didSet { if self.segmentsHidden != oldValue { let transition = ContainedViewLayoutTransition.animated(duration: 0.21, curve: .easeInOut) transition.updateAlpha(node: self.titleNode, alpha: self.segmentsHidden ? 1.0 : 0.0) + transition.updateAlpha(node: self.arrowNode, alpha: self.segmentsHidden ? 1.0 : 0.0) transition.updateAlpha(node: self.segmentedControlNode, alpha: self.segmentsHidden ? 0.0 : 1.0) self.segmentedControlNode.isUserInteractionEnabled = !self.segmentsHidden } @@ -55,14 +73,23 @@ final class MediaPickerTitleView: UIView { } public var indexUpdated: ((Int) -> Void)? + public var action: () -> Void = {} public init(theme: PresentationTheme, segments: [String], selectedIndex: Int) { self.theme = theme self.segments = segments + self.contextSourceNode = ContextReferenceContentNode() + self.buttonNode = HighlightTrackingButtonNode() + self.titleNode = ImmediateTextNode() self.titleNode.displaysAsynchronously = false + self.arrowNode = ASImageNode() + self.arrowNode.displaysAsynchronously = false + self.arrowNode.image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/DownArrow"), color: theme.rootController.navigationBar.secondaryTextColor) + self.arrowNode.isHidden = true + self.segmentedControlNode = SegmentedControlNode(theme: SegmentedControlTheme(theme: theme), items: segments.map { SegmentedControlItem(title: $0) }, selectedIndex: selectedIndex) self.segmentedControlNode.alpha = 0.0 self.segmentedControlNode.isUserInteractionEnabled = false @@ -73,8 +100,26 @@ final class MediaPickerTitleView: UIView { self?.indexUpdated?(index) } + self.buttonNode.highligthedChanged = { [weak self] highlighted in + guard let self else { + return + } + if highlighted { + self.arrowNode.alpha = 0.5 + self.titleNode.alpha = 0.5 + } else { + self.arrowNode.alpha = 1.0 + self.titleNode.alpha = 1.0 + } + } + + self.addSubnode(self.contextSourceNode) self.addSubnode(self.titleNode) + self.addSubnode(self.arrowNode) + self.addSubnode(self.buttonNode) self.addSubnode(self.segmentedControlNode) + + self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside) } required public init?(coder aDecoder: NSCoder) { @@ -85,10 +130,21 @@ final class MediaPickerTitleView: UIView { super.layoutSubviews() let size = self.bounds.size + self.contextSourceNode.frame = self.bounds.insetBy(dx: 0.0, dy: 14.0) + let controlSize = self.segmentedControlNode.updateLayout(.stretchToFill(width: min(300.0, size.width - 36.0)), transition: .immediate) self.segmentedControlNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - controlSize.width) / 2.0), y: floorToScreenPixels((size.height - controlSize.height) / 2.0)), size: controlSize) let titleSize = self.titleNode.updateLayout(CGSize(width: 210.0, height: 44.0)) self.titleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize) + + if let arrowSize = self.arrowNode.image?.size { + self.arrowNode.frame = CGRect(origin: CGPoint(x: self.titleNode.frame.maxX + 5.0, y: floorToScreenPixels((size.height - arrowSize.height) / 2.0) + 1.0 - UIScreenPixel), size: arrowSize) + } + self.buttonNode.frame = CGRect(origin: .zero, size: size) + } + + @objc private func buttonPressed() { + self.action() } } diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index b540e9f0bd..b83c4eb5a8 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -342,7 +342,8 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { UIColor(rgb: 0x548DFF), UIColor(rgb: 0x54A3FF), UIColor(rgb: 0x54bdff), - UIColor(rgb: 0x71c8ff) + UIColor(rgb: 0x71c8ff), + UIColor(rgb: 0xa0daff) ] i = 0 diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index f38ade8550..92a0a34dca 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -360,7 +360,8 @@ public enum PremiumPerk: CaseIterable { .appIcons, .animatedEmoji, .emojiStatus, - .translation + .translation, + .stories ] } @@ -522,6 +523,7 @@ struct PremiumIntroConfiguration { .doubleLimits, .moreUpload, .fasterDownload, + .translation, .voiceToText, .noAds, .emojiStatus, @@ -1614,17 +1616,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { ) context.add(text .position(CGPoint(x: size.width / 2.0, y: size.height + text.size.height / 2.0)) -// .update(Transition.Update { _, view, _ in -// if let snapshot = view.snapshotView(afterScreenUpdates: false) { -// let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) -// view.superview?.addSubview(snapshot) -// transition.setAlpha(view: snapshot, alpha: 0.0, completion: { [weak snapshot] _ in -// snapshot?.removeFromSuperview() -// }) -// snapshot.frame = view.frame -// transition.animateAlpha(view: view, from: 0.0, to: 1.0) -// } -// }) ) size.height += text.size.height size.height += 21.0 @@ -1643,7 +1634,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { UIColor(rgb: 0x548DFF), UIColor(rgb: 0x54A3FF), UIColor(rgb: 0x54bdff), - UIColor(rgb: 0x71c8ff) + UIColor(rgb: 0x71c8ff), + UIColor(rgb: 0xa0daff) ] let accountContext = context.component.context diff --git a/submodules/PremiumUI/Sources/StoriesPageComponent.swift b/submodules/PremiumUI/Sources/StoriesPageComponent.swift index 0114304454..35c03ef352 100644 --- a/submodules/PremiumUI/Sources/StoriesPageComponent.swift +++ b/submodules/PremiumUI/Sources/StoriesPageComponent.swift @@ -309,7 +309,7 @@ private final class StoriesListComponent: CombinedComponent { return { context in let theme = context.component.theme -// let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings + let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings let colors = [ UIColor(rgb: 0x0275f3), @@ -343,9 +343,9 @@ private final class StoriesListComponent: CombinedComponent { AnyComponentWithIdentity( id: "order", component: AnyComponent(ParagraphComponent( - title: "Priority Order", + title: strings.Premium_Stories_Order_Title, titleColor: titleColor, - text: "Get more views as your stories are always displayed first.", + text: strings.Premium_Stories_Order_Text, textColor: textColor, iconName: "Premium/Stories/Order", iconColor: colors[0] @@ -357,9 +357,9 @@ private final class StoriesListComponent: CombinedComponent { AnyComponentWithIdentity( id: "stealth", component: AnyComponent(ParagraphComponent( - title: "Stealth Mode", + title: strings.Premium_Stories_Stealth_Title, titleColor: titleColor, - text: "Hide the fact that you viewed other people's stories.", + text: strings.Premium_Stories_Stealth_Text, textColor: textColor, iconName: "Premium/Stories/Stealth", iconColor: colors[1] @@ -371,9 +371,9 @@ private final class StoriesListComponent: CombinedComponent { AnyComponentWithIdentity( id: "views", component: AnyComponent(ParagraphComponent( - title: "Permanent Views History", + title: strings.Premium_Stories_Views_Title, titleColor: titleColor, - text: "Check who opens your stories — even after they expire.", + text: strings.Premium_Stories_Views_Text, textColor: textColor, iconName: "Premium/Stories/Views", iconColor: colors[2] @@ -385,9 +385,9 @@ private final class StoriesListComponent: CombinedComponent { AnyComponentWithIdentity( id: "expiration", component: AnyComponent(ParagraphComponent( - title: "Expiration Durations", + title: strings.Premium_Stories_Expiration_Title, titleColor: titleColor, - text: "Set custom expiration durations like 6 or 48 hours for your stories.", + text: strings.Premium_Stories_Expiration_Text, textColor: textColor, iconName: "Premium/Stories/Expire", iconColor: colors[3] @@ -399,9 +399,9 @@ private final class StoriesListComponent: CombinedComponent { AnyComponentWithIdentity( id: "save", component: AnyComponent(ParagraphComponent( - title: "Save Stories to Gallery", + title: strings.Premium_Stories_Save_Title, titleColor: titleColor, - text: "Save other people's unprotected stories to your Gallery.", + text: strings.Premium_Stories_Save_Text, textColor: textColor, iconName: "Premium/Stories/Save", iconColor: colors[4] @@ -413,9 +413,9 @@ private final class StoriesListComponent: CombinedComponent { AnyComponentWithIdentity( id: "captions", component: AnyComponent(ParagraphComponent( - title: "Longer Captions", + title: strings.Premium_Stories_Captions_Title, titleColor: titleColor, - text: "Add ten times longer captions to your stories.", + text: strings.Premium_Stories_Captions_Text, textColor: textColor, iconName: "Premium/Stories/Caption", iconColor: colors[5] @@ -427,9 +427,9 @@ private final class StoriesListComponent: CombinedComponent { AnyComponentWithIdentity( id: "format", component: AnyComponent(ParagraphComponent( - title: "Links and Formatting", + title: strings.Premium_Stories_Format_Title, titleColor: titleColor, - text: "Add links and formatting in captions to your stories.", + text: strings.Premium_Stories_Format_Text, textColor: textColor, iconName: "Premium/Stories/Format", iconColor: colors[6] @@ -581,7 +581,7 @@ final class StoriesPageComponent: CombinedComponent { state.isDisplaying = environment.isDisplaying let theme = context.component.theme -// let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings + let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings let topInset: CGFloat = 56.0 @@ -641,7 +641,7 @@ final class StoriesPageComponent: CombinedComponent { let title = title.update( component: MultilineTextComponent( - text: .plain(NSAttributedString(string: "Upgraded Stories", font: Font.semibold(20.0), textColor: theme.rootController.navigationBar.primaryTextColor)), + text: .plain(NSAttributedString(string: strings.Premium_Stories_Title, font: Font.semibold(20.0), textColor: theme.rootController.navigationBar.primaryTextColor)), horizontalAlignment: .center, truncationType: .end, maximumNumberOfLines: 1 @@ -652,7 +652,7 @@ final class StoriesPageComponent: CombinedComponent { let secondaryTitle = secondaryTitle.update( component: MultilineTextComponent( - text: .plain(NSAttributedString(string: "Exclusive Features in Stories", font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)), + text: .plain(NSAttributedString(string: strings.Premium_Stories_AdditionalTitle, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)), horizontalAlignment: .center, truncationType: .end, maximumNumberOfLines: 1 diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 75dbe8e532..3ffced4a04 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -233,7 +233,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private var animationHideNode: Bool = false public var displayTail: Bool = true - public var forceTailToRight: Bool = true + public var forceTailToRight: Bool = false private var didAnimateIn: Bool = false public private(set) var isAnimatingOut: Bool = false diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index f7590d6e0e..ccd562298c 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -708,7 +708,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili } let upload: Signal = .single(nil) |> then( - messageMediaPreuploadManager.upload(network: network, postbox: postbox, source: .resource(fileReference.resourceReference(file.resource)), encrypt: peerId.namespace == Namespaces.Peer.SecretChat, tag: TelegramMediaResourceFetchTag(statsCategory: statsCategoryForFileWithAttributes(file.attributes), userContentType: nil), hintFileSize: hintSize, hintFileIsLarge: hintFileIsLarge) + messageMediaPreuploadManager.upload(network: network, postbox: postbox, source: .resource(fileReference.resourceReference(file.resource)), encrypt: peerId.namespace == Namespaces.Peer.SecretChat, tag: TelegramMediaResourceFetchTag(statsCategory: statsCategoryForFileWithAttributes(file.attributes), userContentType: nil), hintFileSize: hintSize, hintFileIsLarge: hintFileIsLarge, forceNoBigParts: forceNoBigParts) |> mapError { _ -> PendingMessageUploadError in return .generic } |> map(Optional.init) ) diff --git a/submodules/TelegramCore/Sources/State/MessageMediaPreuploadManager.swift b/submodules/TelegramCore/Sources/State/MessageMediaPreuploadManager.swift index 21b60c4c44..c3bc3507b1 100644 --- a/submodules/TelegramCore/Sources/State/MessageMediaPreuploadManager.swift +++ b/submodules/TelegramCore/Sources/State/MessageMediaPreuploadManager.swift @@ -63,7 +63,7 @@ private final class MessageMediaPreuploadManagerContext { })) } - func upload(network: Network, postbox: Postbox, source: MultipartUploadSource, encrypt: Bool, tag: MediaResourceFetchTag?, hintFileSize: Int64?, hintFileIsLarge: Bool) -> Signal { + func upload(network: Network, postbox: Postbox, source: MultipartUploadSource, encrypt: Bool, tag: MediaResourceFetchTag?, hintFileSize: Int64?, hintFileIsLarge: Bool, forceNoBigParts: Bool) -> Signal { let queue = self.queue return Signal { [weak self] subscriber in if let strongSelf = self { @@ -93,7 +93,7 @@ private final class MessageMediaPreuploadManagerContext { } } } else { - return multipartUpload(network: network, postbox: postbox, source: source, encrypt: encrypt, tag: tag, hintFileSize: hintFileSize, hintFileIsLarge: hintFileIsLarge, forceNoBigParts: false).start(next: { next in + return multipartUpload(network: network, postbox: postbox, source: source, encrypt: encrypt, tag: tag, hintFileSize: hintFileSize, hintFileIsLarge: hintFileIsLarge, forceNoBigParts: forceNoBigParts).start(next: { next in subscriber.putNext(next) }, error: { error in subscriber.putError(error) @@ -125,10 +125,10 @@ final class MessageMediaPreuploadManager { } } - func upload(network: Network, postbox: Postbox, source: MultipartUploadSource, encrypt: Bool, tag: MediaResourceFetchTag?, hintFileSize: Int64?, hintFileIsLarge: Bool) -> Signal { + func upload(network: Network, postbox: Postbox, source: MultipartUploadSource, encrypt: Bool, tag: MediaResourceFetchTag?, hintFileSize: Int64?, hintFileIsLarge: Bool, forceNoBigParts: Bool) -> Signal { return Signal, MultipartUploadError> { subscriber in self.impl.with { context in - subscriber.putNext(context.upload(network: network, postbox: postbox, source: source, encrypt: encrypt, tag: tag, hintFileSize: hintFileSize, hintFileIsLarge: hintFileIsLarge)) + subscriber.putNext(context.upload(network: network, postbox: postbox, source: source, encrypt: encrypt, tag: tag, hintFileSize: hintFileSize, hintFileIsLarge: hintFileIsLarge, forceNoBigParts: forceNoBigParts)) subscriber.putCompletion() } return EmptyDisposable diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift index 870eec503f..a2deee0065 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/EngineStoryViewListContext.swift @@ -335,7 +335,7 @@ public final class EngineStoryViewListContext { mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, - views: Stories.Item.Views(seenCount: Int(count), reactedCount: Int(reactionsCount), seenPeerIds: currentViews.seenPeerIds), + views: Stories.Item.Views(seenCount: Int(count), reactedCount: Int(reactionsCount), seenPeerIds: currentViews.seenPeerIds, hasList: currentViews.hasList), privacy: item.privacy, isPinned: item.isPinned, isExpired: item.isExpired, @@ -364,7 +364,7 @@ public final class EngineStoryViewListContext { mediaAreas: item.mediaAreas, text: item.text, entities: item.entities, - views: Stories.Item.Views(seenCount: Int(count), reactedCount: Int(reactionsCount), seenPeerIds: currentViews.seenPeerIds), + views: Stories.Item.Views(seenCount: Int(count), reactedCount: Int(reactionsCount), seenPeerIds: currentViews.seenPeerIds, hasList: currentViews.hasList), privacy: item.privacy, isPinned: item.isPinned, isExpired: item.isExpired, diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index d9a9e176d4..9bfc92e020 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -43,16 +43,19 @@ public enum Stories { case seenCount = "seenCount" case reactedCount = "reactedCount" case seenPeerIds = "seenPeerIds" + case hasList = "hasList" } public var seenCount: Int public var reactedCount: Int public var seenPeerIds: [PeerId] + public var hasList: Bool - public init(seenCount: Int, reactedCount: Int, seenPeerIds: [PeerId]) { + public init(seenCount: Int, reactedCount: Int, seenPeerIds: [PeerId], hasList: Bool) { self.seenCount = seenCount self.reactedCount = reactedCount self.seenPeerIds = seenPeerIds + self.hasList = hasList } public init(from decoder: Decoder) throws { @@ -61,6 +64,7 @@ public enum Stories { self.seenCount = Int(try container.decode(Int32.self, forKey: .seenCount)) self.reactedCount = Int(try container.decodeIfPresent(Int32.self, forKey: .reactedCount) ?? 0) self.seenPeerIds = try container.decode([Int64].self, forKey: .seenPeerIds).map(PeerId.init) + self.hasList = try container.decodeIfPresent(Bool.self, forKey: .hasList) ?? true } public func encode(to encoder: Encoder) throws { @@ -69,6 +73,7 @@ public enum Stories { try container.encode(Int32(clamping: self.seenCount), forKey: .seenCount) try container.encode(Int32(clamping: self.reactedCount), forKey: .reactedCount) try container.encode(self.seenPeerIds.map { $0.toInt64() }, forKey: .seenPeerIds) + try container.encode(self.hasList, forKey: .hasList) } } @@ -1395,12 +1400,13 @@ extension Api.StoryItem { extension Stories.Item.Views { init(apiViews: Api.StoryViews) { switch apiViews { - case let .storyViews(_, viewsCount, reactionsCount, recentViewers): + case let .storyViews(flags, viewsCount, reactionsCount, recentViewers): + let hasList = (flags & (1 << 1)) != 0 var seenPeerIds: [PeerId] = [] if let recentViewers = recentViewers { seenPeerIds = recentViewers.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) } } - self.init(seenCount: Int(viewsCount), reactedCount: Int(reactionsCount), seenPeerIds: seenPeerIds) + self.init(seenCount: Int(viewsCount), reactedCount: Int(reactionsCount), seenPeerIds: seenPeerIds, hasList: hasList) } } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index 68e1322644..df6e06a985 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -15,11 +15,13 @@ public final class EngineStoryItem: Equatable { public let seenCount: Int public let reactedCount: Int public let seenPeers: [EnginePeer] + public let hasList: Bool - public init(seenCount: Int, reactedCount: Int, seenPeers: [EnginePeer]) { + public init(seenCount: Int, reactedCount: Int, seenPeers: [EnginePeer], hasList: Bool) { self.seenCount = seenCount self.reactedCount = reactedCount self.seenPeers = seenPeers + self.hasList = hasList } public static func ==(lhs: Views, rhs: Views) -> Bool { @@ -32,6 +34,9 @@ public final class EngineStoryItem: Equatable { if lhs.seenPeers != rhs.seenPeers { return false } + if lhs.hasList != rhs.hasList { + return false + } return true } } @@ -154,7 +159,8 @@ extension EngineStoryItem { return Stories.Item.Views( seenCount: views.seenCount, reactedCount: views.reactedCount, - seenPeerIds: views.seenPeers.map(\.id) + seenPeerIds: views.seenPeers.map(\.id), + hasList: views.hasList ) }, privacy: self.privacy.flatMap { privacy in @@ -529,7 +535,8 @@ public final class PeerStoryListContext { reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return transaction.getPeer(id).flatMap(EnginePeer.init) - } + }, + hasList: views.hasList ) }, privacy: item.privacy.flatMap(EngineStoryPrivacy.init), @@ -656,7 +663,8 @@ public final class PeerStoryListContext { reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return transaction.getPeer(id).flatMap(EnginePeer.init) - } + }, + hasList: views.hasList ) }, privacy: item.privacy.flatMap(EngineStoryPrivacy.init), @@ -807,7 +815,8 @@ public final class PeerStoryListContext { reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) - } + }, + hasList: views.hasList ) }, privacy: item.privacy.flatMap(EngineStoryPrivacy.init), @@ -849,7 +858,8 @@ public final class PeerStoryListContext { reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) - } + }, + hasList: views.hasList ) }, privacy: item.privacy.flatMap(EngineStoryPrivacy.init), @@ -893,7 +903,8 @@ public final class PeerStoryListContext { reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) - } + }, + hasList: views.hasList ) }, privacy: item.privacy.flatMap(EngineStoryPrivacy.init), @@ -933,7 +944,8 @@ public final class PeerStoryListContext { reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) - } + }, + hasList: views.hasList ) }, privacy: item.privacy.flatMap(EngineStoryPrivacy.init), @@ -1097,7 +1109,8 @@ public final class PeerExpiringStoryListContext { reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return transaction.getPeer(id).flatMap(EnginePeer.init) - } + }, + hasList: views.hasList ) }, privacy: item.privacy.flatMap(EngineStoryPrivacy.init), diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerPhotoUpdater.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerPhotoUpdater.swift index d3c9560b37..e38ab7571a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerPhotoUpdater.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/PeerPhotoUpdater.swift @@ -79,7 +79,7 @@ func _internal_uploadedPeerPhoto(postbox: Postbox, network: Network, resource: M func _internal_uploadedPeerVideo(postbox: Postbox, network: Network, messageMediaPreuploadManager: MessageMediaPreuploadManager?, resource: MediaResource) -> Signal { if let messageMediaPreuploadManager = messageMediaPreuploadManager { - return messageMediaPreuploadManager.upload(network: network, postbox: postbox, source: .resource(.standalone(resource: resource)), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .video, userContentType: .video), hintFileSize: nil, hintFileIsLarge: false) + return messageMediaPreuploadManager.upload(network: network, postbox: postbox, source: .resource(.standalone(resource: resource)), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .video, userContentType: .video), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) |> map { result -> UploadedPeerPhotoData in return UploadedPeerPhotoData(resource: resource, content: .result(result), local: false) } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index ca84139076..3b1f0ef465 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -1171,6 +1171,7 @@ public class CameraScreen: ViewController { } } fileprivate var hasGallery = false + fileprivate var postingAvailable = true private var presentationData: PresentationData private var validLayout: ContainerViewLayout? @@ -2154,7 +2155,7 @@ public class CameraScreen: ViewController { cameraAuthorizationStatus: self.cameraAuthorizationStatus, microphoneAuthorizationStatus: self.microphoneAuthorizationStatus, hasAppeared: self.hasAppeared, - isVisible: self.cameraIsActive && !self.hasGallery, + isVisible: self.cameraIsActive && !self.hasGallery && self.postingAvailable, panelWidth: panelWidth, animateFlipAction: self.animateFlipAction, animateShutter: { [weak self] in @@ -2435,6 +2436,8 @@ public class CameraScreen: ViewController { guard let self, availability != .available else { return } + self.node.postingAvailable = false + let subject: PremiumLimitSubject switch availability { case .expiringLimit: @@ -2462,7 +2465,9 @@ public class CameraScreen: ViewController { return } let isPremium = peer?.isPremium ?? false - if !isPremium { + if isPremium { + self.node.postingAvailable = true + } else { self.requestDismiss(animated: true) } }) @@ -2572,6 +2577,8 @@ public class CameraScreen: ViewController { self.node.hasGallery = false self.node.requestUpdateLayout(hasAppeared: self.node.hasAppeared, transition: .immediate) } + }, groupsPresented: { + stopCameraCapture() }) self.galleryController = controller } diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index bd33ff2c47..c0efa3e5a4 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -2213,7 +2213,7 @@ public protocol EmojiContentPeekBehavior: AnyObject { public protocol EmojiCustomContentView: UIView { var tintContainerView: UIView { get } - func update(theme: PresentationTheme, useOpaqueTheme: Bool, availableSize: CGSize, transition: Transition) -> CGSize + func update(theme: PresentationTheme, strings: PresentationStrings, useOpaqueTheme: Bool, availableSize: CGSize, transition: Transition) -> CGSize } public final class EmojiPagerContentComponent: Component { @@ -6562,7 +6562,7 @@ public final class EmojiPagerContentComponent: Component { } } let availableCustomContentSize = availableSize - let customContentViewSize = customContentView.update(theme: keyboardChildEnvironment.theme, useOpaqueTheme: useOpaqueTheme, availableSize: availableCustomContentSize, transition: customContentViewTransition) + let customContentViewSize = customContentView.update(theme: keyboardChildEnvironment.theme, strings: keyboardChildEnvironment.strings, useOpaqueTheme: useOpaqueTheme, availableSize: availableCustomContentSize, transition: customContentViewTransition) customContentViewTransition.setFrame(view: customContentView, frame: CGRect(origin: CGPoint(x: 0.0, y: pagerEnvironment.containerInsets.top + (component.displaySearchWithPlaceholder != nil ? 54.0 : 0.0)), size: customContentViewSize)) customContentHeight = customContentViewSize.height diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingBubbleEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingBubbleEntity.swift index b8a4ed8990..82b2d2fd3c 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingBubbleEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingBubbleEntity.swift @@ -22,7 +22,7 @@ public final class DrawingBubbleEntity: DrawingEntity, Codable { case stroke } - public let uuid: UUID + public var uuid: UUID public let isAnimated: Bool public var drawType: DrawType @@ -96,8 +96,11 @@ public final class DrawingBubbleEntity: DrawingEntity, Codable { } } - public func duplicate() -> DrawingEntity { + public func duplicate(copy: Bool) -> DrawingEntity { let newEntity = DrawingBubbleEntity(drawType: self.drawType, color: self.color, lineWidth: self.lineWidth) + if copy { + newEntity.uuid = self.uuid + } newEntity.referenceDrawingSize = self.referenceDrawingSize newEntity.position = self.position newEntity.size = self.size diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingEntity.swift index cea001e5dc..025a83a654 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingEntity.swift @@ -2,7 +2,7 @@ import Foundation import UIKit public protocol DrawingEntity: AnyObject { - var uuid: UUID { get } + var uuid: UUID { get set } var isAnimated: Bool { get } var center: CGPoint { get } @@ -13,7 +13,7 @@ public protocol DrawingEntity: AnyObject { var scale: CGFloat { get set } - func duplicate() -> DrawingEntity + func duplicate(copy: Bool) -> DrawingEntity var renderImage: UIImage? { get set } var renderSubEntities: [DrawingEntity]? { get set } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingLocationEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingLocationEntity.swift index 028925d55f..d9eb6d0ea4 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingLocationEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingLocationEntity.swift @@ -11,6 +11,8 @@ public final class DrawingLocationEntity: DrawingEntity, Codable { case uuid case title case style + case color + case hasCustomColor case location case icon case queryId @@ -27,6 +29,7 @@ public final class DrawingLocationEntity: DrawingEntity, Codable { case white case black case transparent + case custom case blur } @@ -42,7 +45,18 @@ public final class DrawingLocationEntity: DrawingEntity, Codable { public var icon: TelegramMediaFile? public var queryId: Int64? public var resultId: String? - public var color: DrawingColor = .clear + public var color: DrawingColor = DrawingColor(color: .white) { + didSet { + if self.color.toUIColor().argb == UIColor.white.argb { + self.style = .white + self.hasCustomColor = false + } else { + self.style = .custom + self.hasCustomColor = true + } + } + } + public var hasCustomColor = false public var lineWidth: CGFloat = 0.0 public var referenceDrawingSize: CGSize @@ -88,6 +102,8 @@ public final class DrawingLocationEntity: DrawingEntity, Codable { self.uuid = try container.decode(UUID.self, forKey: .uuid) self.title = try container.decode(String.self, forKey: .title) self.style = try container.decode(Style.self, forKey: .style) + self.color = try container.decodeIfPresent(DrawingColor.self, forKey: .color) ?? DrawingColor(color: .white) + self.hasCustomColor = try container.decodeIfPresent(Bool.self, forKey: .hasCustomColor) ?? false if let locationData = try container.decodeIfPresent(Data.self, forKey: .location) { self.location = PostboxDecoder(buffer: MemoryBuffer(data: locationData)).decodeRootObject() as! TelegramMediaMap @@ -117,6 +133,8 @@ public final class DrawingLocationEntity: DrawingEntity, Codable { try container.encode(self.uuid, forKey: .uuid) try container.encode(self.title, forKey: .title) try container.encode(self.style, forKey: .style) + try container.encode(self.color, forKey: .color) + try container.encode(self.hasCustomColor, forKey: .hasCustomColor) var encoder = PostboxEncoder() encoder.encodeRootObject(self.location) @@ -143,8 +161,11 @@ public final class DrawingLocationEntity: DrawingEntity, Codable { } } - public func duplicate() -> DrawingEntity { + public func duplicate(copy: Bool) -> DrawingEntity { let newEntity = DrawingLocationEntity(title: self.title, style: self.style, location: self.location, icon: self.icon, queryId: self.queryId, resultId: self.resultId) + if copy { + newEntity.uuid = self.uuid + } newEntity.referenceDrawingSize = self.referenceDrawingSize newEntity.position = self.position newEntity.width = self.width diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingMediaEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingMediaEntity.swift index 896397cbc1..1b90f2c0f5 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingMediaEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingMediaEntity.swift @@ -60,7 +60,7 @@ public final class DrawingMediaEntity: DrawingEntity, Codable { case mirrored } - public let uuid: UUID + public var uuid: UUID public let content: Content public let size: CGSize @@ -157,7 +157,7 @@ public final class DrawingMediaEntity: DrawingEntity, Codable { try container.encode(self.mirrored, forKey: .mirrored) } - public func duplicate() -> DrawingEntity { + public func duplicate(copy: Bool) -> DrawingEntity { let newEntity = DrawingMediaEntity(content: self.content, size: self.size) newEntity.referenceDrawingSize = self.referenceDrawingSize newEntity.position = self.position diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingSimpleShapeEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingSimpleShapeEntity.swift index 2f01566976..9687fe7785 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingSimpleShapeEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingSimpleShapeEntity.swift @@ -28,7 +28,7 @@ public final class DrawingSimpleShapeEntity: DrawingEntity, Codable { case stroke } - public let uuid: UUID + public var uuid: UUID public let isAnimated: Bool public var shapeType: ShapeType @@ -102,8 +102,11 @@ public final class DrawingSimpleShapeEntity: DrawingEntity, Codable { } } - public func duplicate() -> DrawingEntity { + public func duplicate(copy: Bool) -> DrawingEntity { let newEntity = DrawingSimpleShapeEntity(shapeType: self.shapeType, drawType: self.drawType, color: self.color, lineWidth: self.lineWidth) + if copy { + newEntity.uuid = self.uuid + } newEntity.referenceDrawingSize = self.referenceDrawingSize newEntity.position = self.position newEntity.size = self.size diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift index 6a3b369225..641d64bda8 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift @@ -69,7 +69,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { case isExplicitlyStatic } - public let uuid: UUID + public var uuid: UUID public let content: Content public var referenceDrawingSize: CGSize @@ -221,8 +221,11 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { try container.encode(self.isExplicitlyStatic, forKey: .isExplicitlyStatic) } - public func duplicate() -> DrawingEntity { + public func duplicate(copy: Bool) -> DrawingEntity { let newEntity = DrawingStickerEntity(content: self.content) + if copy { + newEntity.uuid = self.uuid + } newEntity.referenceDrawingSize = self.referenceDrawingSize newEntity.position = self.position newEntity.scale = self.scale diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingTextEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingTextEntity.swift index af589e6759..deecc1193a 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingTextEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingTextEntity.swift @@ -247,8 +247,11 @@ public final class DrawingTextEntity: DrawingEntity, Codable { } } - public func duplicate() -> DrawingEntity { + public func duplicate(copy: Bool) -> DrawingEntity { let newEntity = DrawingTextEntity(text: self.text, style: self.style, animation: self.animation, font: self.font, alignment: self.alignment, fontSize: self.fontSize, color: self.color) + if copy { + newEntity.uuid = self.uuid + } newEntity.referenceDrawingSize = self.referenceDrawingSize newEntity.position = self.position newEntity.width = self.width diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingVectorEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingVectorEntity.swift index 42685df5a5..37af80d1bc 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingVectorEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingVectorEntity.swift @@ -23,7 +23,7 @@ public final class DrawingVectorEntity: DrawingEntity, Codable { case twoSidedArrow } - public let uuid: UUID + public var uuid: UUID public let isAnimated: Bool public var type: VectorType @@ -98,8 +98,11 @@ public final class DrawingVectorEntity: DrawingEntity, Codable { } } - public func duplicate() -> DrawingEntity { + public func duplicate(copy: Bool) -> DrawingEntity { let newEntity = DrawingVectorEntity(type: self.type, color: self.color, lineWidth: self.lineWidth) + if copy { + newEntity.uuid = self.uuid + } newEntity.drawingSize = self.drawingSize newEntity.referenceDrawingSize = self.referenceDrawingSize newEntity.start = self.start diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index 9b2776f686..8084b7b4f8 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -503,6 +503,12 @@ public final class MediaEditor { } if let player { + player.isMuted = self.values.videoIsMuted + if let trimRange = self.values.videoTrimRange { + self.player?.currentItem?.forwardPlaybackEndTime = CMTime(seconds: trimRange.upperBound, preferredTimescale: CMTimeScale(1000)) + self.additionalPlayer?.currentItem?.forwardPlaybackEndTime = CMTime(seconds: trimRange.upperBound, preferredTimescale: CMTimeScale(1000)) + } + if let initialSeekPosition = self.initialSeekPosition { self.initialSeekPosition = nil player.seek(to: CMTime(seconds: initialSeekPosition, preferredTimescale: CMTimeScale(1000)), toleranceBefore: .zero, toleranceAfter: .zero) @@ -564,7 +570,10 @@ public final class MediaEditor { } else if case .forceRendering = mode { self.forceRendering = true } - self.values = f(self.values) + let updatedValues = f(self.values) + if self.values != updatedValues { + self.values = updatedValues + } if case .skipRendering = mode { self.skipRendering = false } else if case .forceRendering = mode { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift index a12727b141..bb5a1ac369 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorValues.swift @@ -116,16 +116,24 @@ public final class MediaEditorValues: Codable, Equatable { return false } if let lhsToolValue = lhsToolValue as? Float, let rhsToolValue = rhsToolValue as? Float { - return lhsToolValue != rhsToolValue + if lhsToolValue != rhsToolValue { + return false + } } if let lhsToolValue = lhsToolValue as? BlurValue, let rhsToolValue = rhsToolValue as? BlurValue { - return lhsToolValue != rhsToolValue + if lhsToolValue != rhsToolValue { + return false + } } if let lhsToolValue = lhsToolValue as? TintValue, let rhsToolValue = rhsToolValue as? TintValue { - return lhsToolValue != rhsToolValue + if lhsToolValue != rhsToolValue { + return false + } } if let lhsToolValue = lhsToolValue as? CurvesValue, let rhsToolValue = rhsToolValue as? CurvesValue { - return lhsToolValue != rhsToolValue + if lhsToolValue != rhsToolValue { + return false + } } } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 7620a9d002..8c441d6184 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -1602,7 +1602,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), timeout: 86400, isForwardingDisabled: false, - pin: false + pin: true ) } @@ -1857,7 +1857,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate initialValues = draft.values for entity in draft.values.entities { - self.entitiesView.add(entity.entity, announce: false) + self.entitiesView.add(entity.entity.duplicate(copy: true), announce: false) } if let drawingData = initialValues?.drawing?.pngData() { @@ -2068,6 +2068,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.isInteractingWithEntities = isInteracting if !isInteracting { self.controller?.isSavingAvailable = true + self.hasAnyChanges = true } self.requestUpdate(transition: .easeInOut(duration: 0.2)) } @@ -3231,7 +3232,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if self.entitiesView.selectedEntityView != nil || self.isDisplayingTool { bottomInputOffset = inputHeight / 2.0 } else { - bottomInputOffset = 0.0 //inputHeight - bottomInset - 17.0 + bottomInputOffset = 0.0 } } } @@ -3479,6 +3480,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let stateContext = ShareWithPeersScreen.StateContext( context: self.context, subject: .stories(editing: false), + editing: false, initialPeerIds: Set(privacy.privacy.additionallyIncludePeers), closeFriends: self.closeFriends.get(), blockedPeersContext: self.storiesBlockedPeers @@ -3554,11 +3556,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } else if privacy.base == .nobody { subject = .chats(blocked: false) } else { - subject = .contacts(privacy.base) + subject = .contacts(base: privacy.base) } let stateContext = ShareWithPeersScreen.StateContext( context: self.context, subject: subject, + editing: false, initialPeerIds: Set(privacy.additionallyIncludePeers), blockedPeersContext: self.storiesBlockedPeers ) @@ -3723,7 +3726,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let controller = UndoOverlayController(presentationData: presentationData, content: .linkCopied(text: text), elevatedLayout: false, position: .top, animateInAsReplacement: false, action: { [weak self] action in if case .info = action, let self { - let controller = context.sharedContext.makePremiumIntroController(context: context, source: .stories, forceDark: true, dismissed: nil) + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .storiesFormatting, forceDark: true, dismissed: nil) self.push(controller) } return false } @@ -3967,10 +3970,11 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) } let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) - + var caption = self.getCaption() caption = convertMarkdownToAttributes(caption) + var hasEntityChanges = false let randomId: Int64 if case let .draft(_, id) = subject, let id { randomId = id @@ -3979,7 +3983,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } var mediaAreas: [MediaArea] = [] - if case .draft = subject { + if case let .draft(draft, _) = subject { + if draft.values.entities != codableEntities { + hasEntityChanges = true + } } else { mediaAreas = self.initialMediaAreas ?? [] } @@ -4007,7 +4014,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } - if self.isEditingStory && !self.node.hasAnyChanges { + if self.isEditingStory && !(self.node.hasAnyChanges || hasEntityChanges) { self.completion(randomId, nil, [], caption, self.state.privacy, stickers, { [weak self] finished in self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in self?.dismiss() diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index f4397508c4..2c36d7c134 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -1119,8 +1119,8 @@ public final class MessageInputPanelComponent: Component { theme: component.theme, strings: component.strings, presentController: component.presentController, - audioRecorder: component.audioRecorder, - videoRecordingStatus: component.videoRecordingStatus + audioRecorder: nil, + videoRecordingStatus: nil )), environment: {}, containerSize: CGSize(width: 33.0, height: 33.0) diff --git a/submodules/TelegramUI/Components/NavigationSearchComponent/Sources/NavigationSearchComponent.swift b/submodules/TelegramUI/Components/NavigationSearchComponent/Sources/NavigationSearchComponent.swift index db8614b47e..a2f34b8ab4 100644 --- a/submodules/TelegramUI/Components/NavigationSearchComponent/Sources/NavigationSearchComponent.swift +++ b/submodules/TelegramUI/Components/NavigationSearchComponent/Sources/NavigationSearchComponent.swift @@ -26,6 +26,7 @@ public final class NavigationSearchComponent: Component { } public let colors: Colors + public let cancel: String public let placeholder: String public let isSearchActive: Bool public let collapseFraction: CGFloat @@ -35,6 +36,7 @@ public final class NavigationSearchComponent: Component { public init( colors: Colors, + cancel: String, placeholder: String, isSearchActive: Bool, collapseFraction: CGFloat, @@ -43,6 +45,7 @@ public final class NavigationSearchComponent: Component { updateQuery: @escaping (String) -> Void ) { self.colors = colors + self.cancel = cancel self.placeholder = placeholder self.isSearchActive = isSearchActive self.collapseFraction = collapseFraction @@ -55,6 +58,9 @@ public final class NavigationSearchComponent: Component { if lhs.colors != rhs.colors { return false } + if lhs.cancel != rhs.cancel { + return false + } if lhs.placeholder != rhs.placeholder { return false } @@ -189,11 +195,10 @@ public final class NavigationSearchComponent: Component { self.button = button } - //TODO:localize let buttonSize = button.update( transition: buttonTransition, component: AnyComponent(Button( - content: AnyComponent(Text(text: "Cancel", font: Font.regular(17.0), color: component.colors.button)), + content: AnyComponent(Text(text: component.cancel, font: Font.regular(17.0), color: component.colors.button)), action: { [weak self] in guard let self, let component = self.component else { return diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index 995f99f511..8d30606e17 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -1626,7 +1626,7 @@ final class ShareWithPeersScreenComponent: Component { if let searchStateContext = self.searchStateContext, searchStateContext.subject == .search(query: self.navigationTextFieldState.text, onlyContacts: onlyContacts) { } else { self.searchStateDisposable?.dispose() - let searchStateContext = ShareWithPeersScreen.StateContext(context: component.context, subject: .search(query: self.navigationTextFieldState.text, onlyContacts: onlyContacts)) + let searchStateContext = ShareWithPeersScreen.StateContext(context: component.context, subject: .search(query: self.navigationTextFieldState.text, onlyContacts: onlyContacts), editing: false) var applyState = false self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let self else { @@ -1667,6 +1667,10 @@ final class ShareWithPeersScreenComponent: Component { environment: {}, containerSize: CGSize(width: itemsContainerWidth, height: 1000.0) ) + var isContactsSearch = false + if let searchStateContext = self.searchStateContext, case .search(_, true) = searchStateContext.subject { + isContactsSearch = true + } let peerItemSize = self.peerTemplateItem.update( transition: transition, component: AnyComponent(PeerListItemComponent( @@ -1677,7 +1681,7 @@ final class ShareWithPeersScreenComponent: Component { sideInset: sideInset, title: "Name", peer: nil, - subtitle: self.searchStateContext != nil ? "" : "sub", + subtitle: isContactsSearch ? "" : "sub", subtitleAccessory: .none, presence: nil, selectionState: .editing(isSelected: false, isTinted: false), @@ -1977,7 +1981,9 @@ final class ShareWithPeersScreenComponent: Component { let proceed = { var savePeers = true - if base == .closeFriends { + if component.stateContext.editing { + savePeers = false + } else if base == .closeFriends { savePeers = false } else { if case .stories = component.stateContext.subject { @@ -2023,7 +2029,7 @@ final class ShareWithPeersScreenComponent: Component { } if savePeers { - let _ = (updatePeersListStoredStateInteractively(engine: component.context.engine, base: base, peerIds: self.selectedPeers) + let _ = (updatePeersListStoredState(engine: component.context.engine, base: base, peerIds: self.selectedPeers) |> deliverOnMainQueue).start(completed: { complete() }) @@ -2261,13 +2267,14 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { public enum Subject: Equatable { case stories(editing: Bool) case chats(blocked: Bool) - case contacts(EngineStoryPrivacy.Base) + case contacts(base: EngineStoryPrivacy.Base) case search(query: String, onlyContacts: Bool) } fileprivate var stateValue: State? public let subject: Subject + public let editing: Bool public private(set) var initialPeerIds: Set = Set() fileprivate let blockedPeersContext: BlockedPeersContext? @@ -2284,11 +2291,14 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { public init( context: AccountContext, subject: Subject = .chats(blocked: false), + editing: Bool, + initialSelectedPeers: [EngineStoryPrivacy.Base: [EnginePeer.Id]] = [:], initialPeerIds: Set = Set(), closeFriends: Signal<[EnginePeer], NoError> = .single([]), blockedPeersContext: BlockedPeersContext? = nil ) { self.subject = subject + self.editing = editing self.initialPeerIds = initialPeerIds self.blockedPeersContext = blockedPeersContext @@ -2313,20 +2323,34 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { savedContactsExceptionPeers, savedSelectedPeers ) |> mapToSignal { everyone, contacts, selected -> Signal<([EnginePeer.Id: EnginePeer], [EnginePeer.Id], [EnginePeer.Id], [EnginePeer.Id]), NoError> in + var everyone = everyone + if let initialPeerIds = initialSelectedPeers[.everyone] { + everyone = initialPeerIds + } var everyonePeerSignals: [Signal] = [] if everyone.count < 3 { for peerId in everyone { everyonePeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) } } + + var contacts = contacts + if let initialPeerIds = initialSelectedPeers[.contacts] { + contacts = initialPeerIds + } var contactsPeerSignals: [Signal] = [] if contacts.count < 3 { for peerId in contacts { contactsPeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) } } + + var selected = selected + if let initialPeerIds = initialSelectedPeers[.nobody] { + selected = initialPeerIds + } var selectedPeerSignals: [Signal] = [] - if contacts.count < 3 { + if selected.count < 3 { for peerId in selected { selectedPeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))) } @@ -2576,7 +2600,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { self.readySubject.set(true) }) case let .search(query, onlyContacts): - let signal: Signal<([EngineRenderedPeer], [EnginePeer.Id: Optional]), NoError> + let signal: Signal<([EngineRenderedPeer], [EnginePeer.Id: Optional], [EnginePeer.Id: Optional]), NoError> if onlyContacts { signal = combineLatest( context.engine.contacts.searchLocalPeers(query: query), @@ -2584,25 +2608,33 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { ) |> map { peers, contacts in let contactIds = Set(contacts.0.map { $0.id }) - return (peers.filter { contactIds.contains($0.peerId) }, [:]) + return (peers.filter { contactIds.contains($0.peerId) }, [:], [:]) } } else { signal = context.engine.contacts.searchLocalPeers(query: query) |> mapToSignal { peers in return context.engine.data.subscribe( + EngineDataMap(peers.map(\.peerId).map(TelegramEngine.EngineData.Item.Peer.Presence.init)), EngineDataMap(peers.map(\.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) ) - |> map { participantCountMap -> ([EngineRenderedPeer], [EnginePeer.Id: Optional]) in - return (peers, participantCountMap) + |> map { presenceMap, participantCountMap -> ([EngineRenderedPeer], [EnginePeer.Id: Optional], [EnginePeer.Id: Optional]) in + return (peers, presenceMap, participantCountMap) } } } self.stateDisposable = (signal - |> deliverOnMainQueue).start(next: { [weak self] peers, participantCounts in + |> deliverOnMainQueue).start(next: { [weak self] peers, presenceMap, participantCounts in guard let self else { return } + var presences: [EnginePeer.Id: EnginePeer.Presence] = [:] + for (key, value) in presenceMap { + if let value { + presences[key] = value + } + } + var participants: [EnginePeer.Id: Int] = [:] for (key, value) in participantCounts { if let value { @@ -2638,7 +2670,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { }, peersMap: [:], savedSelectedPeers: [:], - presences: [:], + presences: presences, participants: participants, closeFriendsPeers: [], grayListPeers: [] @@ -2936,7 +2968,7 @@ private func peersListStoredState(engine: TelegramEngine, base: Stories.Item.Pri } } -private func updatePeersListStoredStateInteractively(engine: TelegramEngine, base: Stories.Item.Privacy.Base, peerIds: [EnginePeer.Id]) -> Signal { +private func updatePeersListStoredState(engine: TelegramEngine, base: Stories.Item.Privacy.Base, peerIds: [EnginePeer.Id]) -> Signal { let key = EngineDataBuffer(length: 4) key.setInt32(0, value: base.rawValue) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index 6ddae15928..d38b371e54 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -161,7 +161,8 @@ public final class StoryContentContextImpl: StoryContentContext { reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) - } + }, + hasList: views.hasList ) }, privacy: item.privacy.flatMap(EngineStoryPrivacy.init), @@ -1037,7 +1038,8 @@ public final class SingleStoryContentContextImpl: StoryContentContext { reactedCount: views.reactedCount, seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in return peers[id].flatMap(EnginePeer.init) - } + }, + hasList: views.hasList ) }, privacy: itemValue.privacy.flatMap(EngineStoryPrivacy.init), diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index 20099333b7..060733c7df 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -719,7 +719,7 @@ final class StoryItemContentComponent: Component { if let current = self.loadingEffectView { loadingEffectView = current } else { - loadingEffectView = StoryItemLoadingEffectView(effectAlpha: 0.1, duration: 1.0, hasBorder: true, playOnce: false) + loadingEffectView = StoryItemLoadingEffectView(effectAlpha: 0.1, borderAlpha: 0.2, duration: 1.0, hasCustomBorder: true, playOnce: false) self.loadingEffectView = loadingEffectView self.addSubview(loadingEffectView) } @@ -743,21 +743,21 @@ final class StoryItemContentComponent: Component { if let current = self.mediaAreasEffectView { mediaAreasEffectView = current } else { - mediaAreasEffectView = StoryItemLoadingEffectView(effectAlpha: 0.25, duration: 1.5, hasBorder: false, playOnce: true) + mediaAreasEffectView = StoryItemLoadingEffectView(effectAlpha: 0.35, borderAlpha: 0.45, gradientWidth: 150.0, duration: 1.2, hasCustomBorder: false, playOnce: true) self.mediaAreasEffectView = mediaAreasEffectView self.addSubview(mediaAreasEffectView) } mediaAreasEffectView.update(size: availableSize, transition: transition) - let maskView: UIView - if let current = mediaAreasEffectView.mask { - maskView = current + let maskLayer: CALayer + if let current = mediaAreasEffectView.layer.mask { + maskLayer = current } else { - maskView = UIView(frame: CGRect(origin: .zero, size: availableSize)) - mediaAreasEffectView.mask = maskView + maskLayer = CALayer() + mediaAreasEffectView.layer.mask = maskLayer } - if maskView.subviews.isEmpty { + if (maskLayer.sublayers ?? []).isEmpty { let referenceSize = availableSize for mediaArea in component.item.mediaAreas { guard case .venue = mediaArea else { @@ -765,15 +765,26 @@ final class StoryItemContentComponent: Component { } let size = CGSize(width: mediaArea.coordinates.width / 100.0 * referenceSize.width, height: mediaArea.coordinates.height / 100.0 * referenceSize.height) let position = CGPoint(x: mediaArea.coordinates.x / 100.0 * referenceSize.width, y: mediaArea.coordinates.y / 100.0 * referenceSize.height) + let cornerRadius = size.height * 0.18 - let view = UIView() - view.backgroundColor = .white - view.bounds = CGRect(origin: .zero, size: size) - view.center = position - view.layer.cornerRadius = size.height * 0.18 - maskView.addSubview(view) + let layer = CALayer() + layer.backgroundColor = UIColor.white.cgColor + layer.bounds = CGRect(origin: .zero, size: size) + layer.position = position + layer.cornerRadius = cornerRadius + maskLayer.addSublayer(layer) - view.transform = CGAffineTransformMakeRotation(mediaArea.coordinates.rotation * Double.pi / 180.0) + let borderLayer = CAShapeLayer() + borderLayer.strokeColor = UIColor.white.cgColor + borderLayer.fillColor = UIColor.clear.cgColor + borderLayer.lineWidth = 2.0 + borderLayer.path = CGPath(roundedRect: CGRect(origin: .zero, size: size), cornerWidth: cornerRadius, cornerHeight: cornerRadius, transform: nil) + borderLayer.bounds = CGRect(origin: .zero, size: size) + borderLayer.position = position + mediaAreasEffectView.borderMaskLayer.addSublayer(borderLayer) + + layer.transform = CATransform3DMakeRotation(mediaArea.coordinates.rotation * Double.pi / 180.0, 0.0, 0.0, 1.0) + borderLayer.transform = layer.transform } } } else if let mediaAreasEffectView = self.mediaAreasEffectView { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift index c36392cc32..aa7e12031e 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemLoadingEffectView.swift @@ -5,8 +5,8 @@ import ComponentFlow import Display final class StoryItemLoadingEffectView: UIView { - private let effectAlpha: CGFloat private let duration: Double + private let hasCustomBorder: Bool private let playOnce: Bool private let hierarchyTrackingLayer: HierarchyTrackingLayer @@ -16,18 +16,18 @@ final class StoryItemLoadingEffectView: UIView { private let borderGradientView: UIImageView private let borderContainerView: UIView - private let borderMaskLayer: SimpleShapeLayer + let borderMaskLayer: SimpleShapeLayer private var didPlayOnce = false - init(effectAlpha: CGFloat, duration: Double, hasBorder: Bool, playOnce: Bool) { + init(effectAlpha: CGFloat, borderAlpha: CGFloat, gradientWidth: CGFloat = 200.0, duration: Double, hasCustomBorder: Bool, playOnce: Bool) { self.hierarchyTrackingLayer = HierarchyTrackingLayer() - self.effectAlpha = effectAlpha self.duration = duration + self.hasCustomBorder = hasCustomBorder self.playOnce = playOnce - self.gradientWidth = 200.0 + self.gradientWidth = gradientWidth self.backgroundView = UIImageView() self.borderGradientView = UIImageView() @@ -75,15 +75,13 @@ final class StoryItemLoadingEffectView: UIView { context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: size.width, y: 0.0), options: CGGradientDrawingOptions()) }) } - self.backgroundView.image = generateGradient(self.effectAlpha) + self.backgroundView.image = generateGradient(effectAlpha) self.addSubview(self.backgroundView) - if hasBorder { - self.borderGradientView.image = generateGradient(self.effectAlpha + 0.1) - self.borderContainerView.addSubview(self.borderGradientView) - self.addSubview(self.borderContainerView) - self.borderContainerView.layer.mask = self.borderMaskLayer - } + self.borderGradientView.image = generateGradient(borderAlpha) + self.borderContainerView.addSubview(self.borderGradientView) + self.addSubview(self.borderContainerView) + self.borderContainerView.layer.mask = self.borderMaskLayer } required init?(coder: NSCoder) { @@ -107,11 +105,13 @@ final class StoryItemLoadingEffectView: UIView { if self.backgroundView.bounds.size != size { self.backgroundView.layer.removeAllAnimations() - self.borderMaskLayer.fillColor = nil - self.borderMaskLayer.strokeColor = UIColor.white.cgColor - let lineWidth: CGFloat = 3.0 - self.borderMaskLayer.lineWidth = lineWidth - self.borderMaskLayer.path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5), cornerRadius: 12.0).cgPath + if !self.hasCustomBorder { + self.borderMaskLayer.fillColor = nil + self.borderMaskLayer.strokeColor = UIColor.white.cgColor + let lineWidth: CGFloat = 3.0 + self.borderMaskLayer.lineWidth = lineWidth + self.borderMaskLayer.path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(), size: size).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5), cornerRadius: 12.0).cgPath + } } transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: size.height))) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index e963eb88a8..6061714ea9 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -390,6 +390,7 @@ public final class StoryItemSetContainerComponent: Component { private let scroller: Scroller let componentContainerView: UIView + let overlayContainerView: SparseContainerView let itemsContainerView: UIView let controlsContainerView: UIView let controlsClippingView: UIView @@ -440,6 +441,7 @@ public final class StoryItemSetContainerComponent: Component { weak var disappearingReactionContextNode: ReactionContextNode? var displayLikeReactions: Bool = false var waitingForReactionAnimateOutToLike: MessageReaction.Reaction? + private weak var standaloneReactionAnimation: StandaloneReactionAnimation? weak var contextController: ContextController? weak var privacyController: ShareWithPeersScreen? @@ -473,6 +475,7 @@ public final class StoryItemSetContainerComponent: Component { self.sendMessageContext = StoryItemSetContainerSendMessage() self.componentContainerView = UIView() + self.overlayContainerView = SparseContainerView() self.itemsContainerView = UIView() @@ -514,6 +517,7 @@ public final class StoryItemSetContainerComponent: Component { super.init(frame: frame) self.addSubview(self.componentContainerView) + self.addSubview(self.overlayContainerView) self.itemsContainerView.addSubview(self.scroller) self.scroller.delegate = self @@ -530,7 +534,7 @@ public final class StoryItemSetContainerComponent: Component { self.componentContainerView.addSubview(self.viewListsContainer) self.closeButton.addSubview(self.closeButtonIconView) - self.addSubview(self.closeButton) + self.overlayContainerView.addSubview(self.closeButton) self.closeButton.addTarget(self, action: #selector(self.closePressed), for: .touchUpInside) let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) @@ -548,9 +552,19 @@ public final class StoryItemSetContainerComponent: Component { } } + if self.reactionContextNode != nil { + return [] + } + if hasFirstResponder(self) { + return [] + } + if self.itemsContainerView.frame.contains(point) { - if !self.isPointInsideContentArea(point: point) { - return [] + if self.viewListDisplayState != .hidden { + } else { + if !self.isPointInsideContentArea(point: point) { + return [] + } } } @@ -840,6 +854,7 @@ public final class StoryItemSetContainerComponent: Component { @objc private func tapGesture(_ recognizer: UITapGestureRecognizer) { if case .ended = recognizer.state, let component = self.component, let itemLayout = self.itemLayout { + if let _ = self.sendMessageContext.menuController { return } @@ -882,26 +897,29 @@ public final class StoryItemSetContainerComponent: Component { } else { let referenceSize = self.controlsContainerView.frame.size let point = recognizer.location(in: self.controlsContainerView) - - var selectedMediaArea: MediaArea? - - func isPoint(_ point: CGPoint, in area: MediaArea) -> Bool { - let tx = point.x - area.coordinates.x / 100.0 * referenceSize.width - let ty = point.y - area.coordinates.y / 100.0 * referenceSize.height - - let rad = -area.coordinates.rotation * Double.pi / 180.0 - let cosTheta = cos(rad) - let sinTheta = sin(rad) - let rotatedX = tx * cosTheta - ty * sinTheta - let rotatedY = tx * sinTheta + ty * cosTheta - - return abs(rotatedX) <= area.coordinates.width / 100.0 * referenceSize.width / 2.0 * 1.1 && abs(rotatedY) <= area.coordinates.height / 100.0 * referenceSize.height / 2.0 * 1.1 - } - for area in component.slice.item.storyItem.mediaAreas { - if isPoint(point, in: area) { - selectedMediaArea = area - break + var selectedMediaArea: MediaArea? + + let safeAreaInset: CGFloat = 48.0 + if point.x > safeAreaInset && point.x < referenceSize.width - safeAreaInset { + func isPoint(_ point: CGPoint, in area: MediaArea) -> Bool { + let tx = point.x - area.coordinates.x / 100.0 * referenceSize.width + let ty = point.y - area.coordinates.y / 100.0 * referenceSize.height + + let rad = -area.coordinates.rotation * Double.pi / 180.0 + let cosTheta = cos(rad) + let sinTheta = sin(rad) + let rotatedX = tx * cosTheta - ty * sinTheta + let rotatedY = tx * sinTheta + ty * cosTheta + + return abs(rotatedX) <= area.coordinates.width / 100.0 * referenceSize.width / 2.0 * 1.1 && abs(rotatedY) <= area.coordinates.height / 100.0 * referenceSize.height / 2.0 * 1.1 + } + + for area in component.slice.item.storyItem.mediaAreas { + if isPoint(point, in: area) { + selectedMediaArea = area + break + } } } @@ -1636,6 +1654,10 @@ public final class StoryItemSetContainerComponent: Component { footerAlpha = 0.0 } + if case .regular = component.metrics.widthClass { + footerAlpha *= itemAlpha + } + itemTransition.setAlpha(view: footerPanelView, alpha: footerAlpha) } } else if let footerPanel = visibleItem.footerPanel { @@ -2099,6 +2121,12 @@ public final class StoryItemSetContainerComponent: Component { removeOnCompletion: false ) + self.overlayContainerView.clipsToBounds = true + let overlayToFrame = sourceLocalFrame + let overlayToBounds = CGRect(origin: CGPoint(x: overlayToFrame.minX, y: overlayToFrame.minY), size: overlayToFrame.size) + self.overlayContainerView.layer.animatePosition(from: CGPoint(), to: overlayToFrame.center.offsetBy(dx: -self.overlayContainerView.center.x, dy: -self.overlayContainerView.center.y), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) + self.overlayContainerView.layer.animateBounds(from: self.overlayContainerView.bounds, to: overlayToBounds, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + if !transitionOut.destinationIsAvatar { let transitionView = transitionOut.transitionView @@ -2216,11 +2244,10 @@ public final class StoryItemSetContainerComponent: Component { return } - //TODO:localize let tooltipScreen = TooltipScreen( account: component.context.account, sharedContext: component.context.sharedContext, - text: .markdown(text: "Long tap for more reactions"), + text: .markdown(text: component.strings.Story_LongTapForMoreReactions), balancedTextLayout: true, style: .customBlur(component.theme.rootController.navigationBar.blurredBackgroundColor, 0.0), arrowStyle: .small, @@ -2268,6 +2295,24 @@ public final class StoryItemSetContainerComponent: Component { tooltipScreen.dismiss() } } + + if let standaloneReactionAnimation = self.standaloneReactionAnimation { + self.standaloneReactionAnimation = nil + standaloneReactionAnimation.view.removeFromSuperview() + } + self.displayLikeReactions = false + if let reactionContextNode = self.reactionContextNode { + self.reactionContextNode = nil + + let reactionTransition = Transition.immediate + reactionTransition.setAlpha(view: reactionContextNode.view, alpha: 0.0, completion: { [weak reactionContextNode] _ in + reactionContextNode?.view.removeFromSuperview() + }) + } + if let disappearingReactionContextNode = self.disappearingReactionContextNode { + self.disappearingReactionContextNode = nil + disappearingReactionContextNode.view.removeFromSuperview() + } } var itemsTransition = transition var resetScrollingOffsetWithItemTransition = false @@ -2349,6 +2394,10 @@ public final class StoryItemSetContainerComponent: Component { transition.setBounds(view: self.componentContainerView, bounds: CGRect(origin: CGPoint(), size: availableSize)) transition.setScale(view: self.componentContainerView, scale: dismissPanScale) + transition.setPosition(view: self.overlayContainerView, position: CGPoint(x: availableSize.width * 0.5, y: availableSize.height * 0.5 + dismissPanOffset)) + transition.setBounds(view: self.overlayContainerView, bounds: CGRect(origin: CGPoint(), size: availableSize)) + transition.setScale(view: self.overlayContainerView, scale: dismissPanScale) + var bottomContentInset: CGFloat if !component.safeInsets.bottom.isZero { bottomContentInset = component.safeInsets.bottom + 1.0 @@ -2380,8 +2429,7 @@ public final class StoryItemSetContainerComponent: Component { let inputPlaceholder: String if let stealthModeTimeout = component.stealthModeTimeout { - //TODO:localize - inputPlaceholder = "Stealth Mode active – \(stringForDuration(stealthModeTimeout))" + inputPlaceholder = component.strings.Story_StealthModeActivePlaceholder("\(stringForDuration(stealthModeTimeout))").string } else { inputPlaceholder = component.strings.Story_InputPlaceholderReplyPrivately } @@ -2915,7 +2963,7 @@ public final class StoryItemSetContainerComponent: Component { } if isBlockedFromStories { - itemList.append(.action(ContextMenuActionItem(text: "Show My Stories To \(peer.compactDisplayTitle)", icon: { theme in + itemList.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextShowStoriesTo(peer.compactDisplayTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Stories"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) @@ -2929,7 +2977,7 @@ public final class StoryItemSetContainerComponent: Component { let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) self.component?.presentController(UndoOverlayController( presentationData: presentationData, - content: .info(title: nil, text: "**\(peer.compactDisplayTitle)** will now see your stories.", timeout: nil), + content: .info(title: nil, text: component.strings.Story_ToastShowStoriesTo(peer.compactDisplayTitle).string, timeout: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, @@ -2938,7 +2986,7 @@ public final class StoryItemSetContainerComponent: Component { ), nil) }))) } else { - itemList.append(.action(ContextMenuActionItem(text: "Hide My Stories From \(peer.compactDisplayTitle)", icon: { theme in + itemList.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextHideStoriesFrom(peer.compactDisplayTitle).string, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Stories"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) @@ -2950,7 +2998,7 @@ public final class StoryItemSetContainerComponent: Component { let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) self.component?.presentController(UndoOverlayController( presentationData: presentationData, - content: .info(title: nil, text: "**\(peer.compactDisplayTitle)** will not see your stories anymore.", timeout: nil), + content: .info(title: nil, text: component.strings.Story_ToastHideStoriesFrom(peer.compactDisplayTitle).string, timeout: nil), elevatedLayout: false, position: .top, animateInAsReplacement: false, @@ -2961,8 +3009,7 @@ public final class StoryItemSetContainerComponent: Component { } if isContact { - //TODO:localize - itemList.append(.action(ContextMenuActionItem(text: "Delete Contact", textColor: .destructive, icon: { theme in + itemList.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextDeleteContact, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { [weak self] _, f in f(.default) @@ -2984,8 +3031,8 @@ public final class StoryItemSetContainerComponent: Component { "info2.info2.Fill": animationBackgroundColor ], title: nil, - text: "**\(peer.compactDisplayTitle)** has been removed from your contacts.", - customUndoText: "Undo", + text: component.strings.Story_ToastDeletedContact(peer.compactDisplayTitle).string, + customUndoText: component.strings.Undo_Undo, timeout: nil ), elevatedLayout: false, @@ -3031,8 +3078,8 @@ public final class StoryItemSetContainerComponent: Component { "info2.info2.Fill": animationBackgroundColor ], title: nil, - text: "**\(peer.compactDisplayTitle)** has been blocked.", - customUndoText: "Undo", + text: component.strings.Story_ToastUserBlocked(peer.compactDisplayTitle).string, + customUndoText: component.strings.Undo_Undo, timeout: nil ), elevatedLayout: false, @@ -3257,6 +3304,7 @@ public final class StoryItemSetContainerComponent: Component { closeButtonFrame.origin.y -= contentBottomInsetOverflow transition.setFrame(view: self.closeButton, frame: closeButtonFrame) + transition.setAlpha(view: self.closeButton, alpha: (component.hideUI || self.isEditingStory) ? 0.0 : 1.0) transition.setFrame(view: self.closeButtonIconView, frame: CGRect(origin: CGPoint(x: floor((closeButtonFrame.width - image.size.width) * 0.5), y: floor((closeButtonFrame.height - image.size.height) * 0.5)), size: image.size)) headerRightOffset -= 51.0 } @@ -3908,6 +3956,13 @@ public final class StoryItemSetContainerComponent: Component { guard let self else { return } + + if let standaloneReactionAnimation = self.standaloneReactionAnimation { + self.standaloneReactionAnimation = nil + standaloneReactionAnimation.view.removeFromSuperview() + } + self.standaloneReactionAnimation = standaloneReactionAnimation + standaloneReactionAnimation.frame = self.bounds self.addSubview(standaloneReactionAnimation.view) }, completion: { [weak targetView, weak reactionContextNode] in @@ -4085,6 +4140,13 @@ public final class StoryItemSetContainerComponent: Component { guard let self else { return } + + if let standaloneReactionAnimation = self.standaloneReactionAnimation { + self.standaloneReactionAnimation = nil + standaloneReactionAnimation.view.removeFromSuperview() + } + self.standaloneReactionAnimation = standaloneReactionAnimation + standaloneReactionAnimation.frame = self.bounds self.componentContainerView.addSubview(standaloneReactionAnimation.view) }, completion: { [weak reactionContextNode] in @@ -4271,22 +4333,34 @@ public final class StoryItemSetContainerComponent: Component { self.component?.presentController(controller, nil) } - private func openItemPrivacySettings(initialPrivacy: EngineStoryPrivacy? = nil) { + private func openItemPrivacySettings(updatedPrivacy: EngineStoryPrivacy? = nil) { guard let component = self.component else { return } let context = component.context + let currentPrivacy = component.slice.item.storyItem.privacy ?? EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []) - let privacy = initialPrivacy ?? self.component?.slice.item.storyItem.privacy + if component.slice.item.storyItem.privacy == nil { + Logger.shared.log("EditStoryPrivacy", "Story privacy is unknown") + } + + let privacy = updatedPrivacy ?? component.slice.item.storyItem.privacy guard let privacy else { return } + var selectedPeers: [EngineStoryPrivacy.Base: [EnginePeer.Id]] = [:] + selectedPeers[currentPrivacy.base] = currentPrivacy.additionallyIncludePeers + if let updatedPrivacy { + selectedPeers[updatedPrivacy.base] = updatedPrivacy.additionallyIncludePeers + } + let stateContext = ShareWithPeersScreen.StateContext( context: context, subject: .stories(editing: true), - initialPeerIds: Set(privacy.additionallyIncludePeers), + editing: true, + initialSelectedPeers: selectedPeers, closeFriends: component.closeFriends.get(), blockedPeersContext: component.blockedPeers ) @@ -4303,6 +4377,8 @@ public final class StoryItemSetContainerComponent: Component { return } if component.slice.item.storyItem.privacy == privacy { + self.privacyController = nil + self.updateIsProgressPaused() return } let _ = component.context.engine.messages.editStoryPrivacy(id: component.slice.item.storyItem.id, privacy: privacy).start() @@ -4321,7 +4397,7 @@ public final class StoryItemSetContainerComponent: Component { guard let self else { return } - self.openItemPrivacySettings(initialPrivacy: privacy) + self.openItemPrivacySettings(updatedPrivacy: privacy) }) }, editBlockedPeers: { [weak self] privacy, _, _ in @@ -4332,7 +4408,7 @@ public final class StoryItemSetContainerComponent: Component { guard let self else { return } - self.openItemPrivacySettings(initialPrivacy: privacy) + self.openItemPrivacySettings(updatedPrivacy: privacy) }) } ) @@ -4360,11 +4436,12 @@ public final class StoryItemSetContainerComponent: Component { } else if privacy.base == .nobody { subject = .chats(blocked: false) } else { - subject = .contacts(privacy.base) + subject = .contacts(base: privacy.base) } let stateContext = ShareWithPeersScreen.StateContext( context: context, subject: subject, + editing: true, initialPeerIds: Set(privacy.additionallyIncludePeers), blockedPeersContext: component.blockedPeers ) @@ -4737,7 +4814,6 @@ public final class StoryItemSetContainerComponent: Component { self.dismissAllTooltips() - //TODO:localize let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) let animationBackgroundColor = presentationData.theme.rootController.tabBar.backgroundColor component.presentController(UndoOverlayController( @@ -4750,7 +4826,7 @@ public final class StoryItemSetContainerComponent: Component { "info2.info2.Fill": animationBackgroundColor ], title: nil, - text: "Subscribe to [Telegram Premium]() to save other people's unprotected stories to your Gallery.", + text: component.strings.Story_ToastPremiumSaveToGallery, customUndoText: nil, timeout: nil ), @@ -4788,9 +4864,8 @@ public final class StoryItemSetContainerComponent: Component { } let context = component.context - //TODO:localize var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitsListScreen(context: context, subject: .stories, source: .other, order: [.stories], buttonText: "Upgrade Stories", isPremium: false, forceDark: true) + let controller = PremiumLimitsListScreen(context: context, subject: .stories, source: .other, order: [.stories], buttonText: component.strings.Story_PremiumUpgradeStoriesButton, isPremium: false, forceDark: true) controller.action = { [weak self] in guard let self else { return @@ -4920,7 +4995,14 @@ public final class StoryItemSetContainerComponent: Component { } let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: nil, useDirectRendering: false) - self.componentContainerView.addSubnode(standaloneReactionAnimation) + self.componentContainerView.addSubview(standaloneReactionAnimation.view) + + if let standaloneReactionAnimation = self.standaloneReactionAnimation { + self.standaloneReactionAnimation = nil + standaloneReactionAnimation.view.removeFromSuperview() + } + self.standaloneReactionAnimation = standaloneReactionAnimation + standaloneReactionAnimation.frame = self.bounds standaloneReactionAnimation.animateReactionSelection( context: component.context, @@ -4937,11 +5019,17 @@ public final class StoryItemSetContainerComponent: Component { return } + if let standaloneReactionAnimation = self.standaloneReactionAnimation { + self.standaloneReactionAnimation = nil + standaloneReactionAnimation.view.removeFromSuperview() + } + self.standaloneReactionAnimation = standaloneReactionAnimation + standaloneReactionAnimation.frame = self.bounds - self.componentContainerView.addSubnode(standaloneReactionAnimation) + self.componentContainerView.addSubview(standaloneReactionAnimation.view) }, completion: { [weak standaloneReactionAnimation] in - standaloneReactionAnimation?.removeFromSupernode() + standaloneReactionAnimation?.view.removeFromSuperview() } ) } @@ -5205,8 +5293,7 @@ public final class StoryItemSetContainerComponent: Component { }))) if case let .user(accountUser) = component.slice.peer { - //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Stealth Mode", icon: { theme in + items.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextStealthMode, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: accountUser.isPremium ? "Chat/Context Menu/Eye" : "Chat/Context Menu/EyeLocked"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in a(.default) @@ -5437,8 +5524,7 @@ public final class StoryItemSetContainerComponent: Component { }))) } - //TODO:localize - items.append(.action(ContextMenuActionItem(text: "Stealth Mode", icon: { theme in + items.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextStealthMode, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: accountUser.isPremium ? "Chat/Context Menu/Eye" : "Chat/Context Menu/EyeLocked"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in a(.default) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index a11ffe3ecf..fd7737d758 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -503,20 +503,19 @@ final class StoryItemSetContainerSendMessage { } let timestamp = Int32(Date().timeIntervalSince1970) - if noticeCount < 3, let activeUntilTimestamp = config.stealthModeState.actualizedNow().activeUntilTimestamp, activeUntilTimestamp > timestamp { + if noticeCount < 1, let activeUntilTimestamp = config.stealthModeState.actualizedNow().activeUntilTimestamp, activeUntilTimestamp > timestamp { let theme = component.theme let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) - //TODO:localize let alertController = textAlertController( context: component.context, updatedPresentationData: updatedPresentationData, - title: "You are in Stealth Mode now", - text: "If you send a reply or reaction, the creator of the story will also see you in the list of viewers.", + title: component.strings.Story_AlertStealthModeActiveTitle, + text: component.strings.Story_AlertStealthModeActiveText, actions: [ - TextAlertAction(type: .defaultAction, title: "Cancel", action: {}), - TextAlertAction(type: .genericAction, title: "Proceed", action: { + TextAlertAction(type: .defaultAction, title: component.strings.Common_Cancel, action: {}), + TextAlertAction(type: .genericAction, title: component.strings.Story_AlertStealthModeActiveAction, action: { action() }) ] @@ -532,6 +531,11 @@ final class StoryItemSetContainerSendMessage { view.updateIsProgressPaused() component.controller()?.presentInGlobalOverlay(alertController) + + #if DEBUG + #else + let _ = ApplicationSpecificNotice.incrementStoryStealthModeReplyCount(accountManager: component.context.sharedContext.accountManager).start() + #endif } else { action() } @@ -833,7 +837,12 @@ final class StoryItemSetContainerSendMessage { self.videoRecorder.set(.single(nil)) - self.sendMessages(view: view, peer: peer, messages: [updatedMessage]) + self.performWithPossibleStealthModeConfirmation(view: view, action: { [weak self, weak view] in + guard let self, let view else { + return + } + self.sendMessages(view: view, peer: peer, messages: [updatedMessage]) + }) }, displaySlowmodeTooltip: { [weak self] view, rect in //self?.interfaceInteraction?.displaySlowmodeTooltip(view, rect) let _ = self @@ -878,9 +887,14 @@ final class StoryItemSetContainerSendMessage { let waveformBuffer: Data? = data.waveform - self.sendMessages(view: view, peer: peer, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) - - HapticFeedback().tap() + self.performWithPossibleStealthModeConfirmation(view: view, action: { [weak self, weak view] in + guard let self, let view else { + return + } + self.sendMessages(view: view, peer: peer, messages: [.message(text: "", attributes: [], inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: EngineMedia.Id(namespace: Namespaces.Media.LocalFile, id: randomId), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(data.compressedData.count), attributes: [.Audio(isVoice: true, duration: Int(data.duration), title: nil, performer: nil, waveform: waveformBuffer)])), replyToMessageId: nil, replyToStoryId: focusedStoryId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]) + + HapticFeedback().tap() + }) } }) } else if let videoRecorderValue = self.videoRecorderValue { @@ -3059,11 +3073,10 @@ final class StoryItemSetContainerSendMessage { let remainingActiveSeconds = activeUntilTimestamp - timestamp let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) - //TODO:localize - let text = "The creators of stories you will view in the next **\(timeIntervalString(strings: presentationData.strings, value: remainingActiveSeconds))** won't see you in the viewers' lists." + let text = component.strings.Story_ToastStealthModeActiveText(timeIntervalString(strings: presentationData.strings, value: remainingActiveSeconds)).string let tooltipScreen = UndoOverlayController( presentationData: presentationData, - content: .actionSucceeded(title: "You are in Stealth Mode", text: text, cancel: "", destructive: false), + content: .actionSucceeded(title: component.strings.Story_ToastStealthModeActiveTitle, text: text, cancel: "", destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in @@ -3087,9 +3100,8 @@ final class StoryItemSetContainerSendMessage { let remainingActiveSeconds = max(1, activeUntilTimestamp - timestamp) let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) - //TODO:localize - let text = "The creators of stories you will view in the next **\(timeIntervalString(strings: presentationData.strings, value: remainingActiveSeconds))** won't see you in the viewers' lists." - tooltipScreenValue.content = .actionSucceeded(title: "You are in Stealth Mode", text: text, cancel: "", destructive: false) + let text = component.strings.Story_ToastStealthModeActiveText(timeIntervalString(strings: presentationData.strings, value: remainingActiveSeconds)).string + tooltipScreenValue.content = .actionSucceeded(title: component.strings.Story_ToastStealthModeActiveTitle, text: text, cancel: "", destructive: false) }) self.tooltipScreen?.dismiss(animated: true) @@ -3128,11 +3140,10 @@ final class StoryItemSetContainerSendMessage { } let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme) - //TODO:localize - let text = "The creators of stories you viewed in the last \(timeIntervalString(strings: presentationData.strings, value: pastPeriod)) or will view in the next **\(timeIntervalString(strings: presentationData.strings, value: futurePeriod))** won’t see you in the viewers’ lists." + let text = component.strings.Story_ToastStealthModeActivatedText(timeIntervalString(strings: presentationData.strings, value: pastPeriod), timeIntervalString(strings: presentationData.strings, value: futurePeriod)).string let tooltipScreen = UndoOverlayController( presentationData: presentationData, - content: .actionSucceeded(title: "Stealth Mode On", text: text, cancel: "", destructive: false), + content: .actionSucceeded(title: component.strings.Story_ToastStealthModeActivatedTitle, text: text, cancel: "", destructive: false), elevatedLayout: false, animateInAsReplacement: false, action: { _ in @@ -3222,7 +3233,7 @@ final class StoryItemSetContainerSendMessage { let subject = EngineMessage(stableId: 0, stableVersion: 0, id: EngineMessage.Id(peerId: PeerId(0), namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: [.geo(TelegramMediaMap(latitude: venue.latitude, longitude: venue.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: venue.venue, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil))], peers: [:], associatedMessages: [:], associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:]) let context = component.context - actions.append(ContextMenuAction(content: .textWithIcon(title: "View Location", icon: generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: .white)), action: { [weak controller, weak view] in + actions.append(ContextMenuAction(content: .textWithIcon(title: updatedPresentationData.initial.strings.Story_ViewLocation, icon: generateTintedImage(image: UIImage(bundleImageName: "Settings/TextArrowRight"), color: .white)), action: { [weak controller, weak view] in let locationController = LocationViewController( context: context, updatedPresentationData: updatedPresentationData, diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index 3d2972fedb..29de8c3989 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -586,6 +586,9 @@ final class StoryItemSetViewListComponent: Component { if let baseContentView, baseContentView.configuration == self.configuration, baseContentView.query == nil { parentSource = baseContentView.viewList } + if component.context.sharedContext.immediateExperimentalUISettings.storiesExperiment { + parentSource = nil + } self.viewList = component.context.engine.messages.storyViewList(id: component.storyItem.id, views: views, listMode: mappedListMode, sortMode: mappedSortMode, searchQuery: query, parentSource: parentSource) } @@ -753,7 +756,7 @@ final class StoryItemSetViewListComponent: Component { } var premiumFooterSize: CGSize? - if !component.hasPremium, let viewListState = self.viewListState, viewListState.loadMoreToken == nil, !viewListState.items.isEmpty, let views = component.storyItem.views, views.seenCount > viewListState.totalCount, component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) { + if self.configuration.listMode == .everyone, let viewListState = self.viewListState, viewListState.loadMoreToken == nil, !viewListState.items.isEmpty, let views = component.storyItem.views, views.seenCount > viewListState.totalCount, component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) { let premiumFooterText: ComponentView if let current = self.premiumFooterText { premiumFooterText = current @@ -768,8 +771,15 @@ final class StoryItemSetViewListComponent: Component { let link = MarkdownAttributeSet(font: Font.semibold(fontSize), textColor: component.theme.list.itemAccentColor) let attributes = MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return ("URL", "") }) - //TODO:localize - let text = "To unlock viewers' lists for expired and saved stories, subscribe to [Telegram Premium]()." + let text: String + let fullWidth: Bool + if component.hasPremium { + text = component.strings.Story_ViewList_NotFullyRecorded + fullWidth = true + } else { + text = component.strings.Story_ViewList_PremiumUpgradeInlineText + fullWidth = false + } premiumFooterSize = premiumFooterText.update( transition: .immediate, component: AnyComponent(BalancedTextComponent( @@ -778,14 +788,14 @@ final class StoryItemSetViewListComponent: Component { maximumNumberOfLines: 0, lineSpacing: 0.2, highlightColor: component.theme.list.itemAccentColor.withMultipliedAlpha(0.5), - highlightAction: { attributes in + highlightAction: component.hasPremium ? nil : { attributes in if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] { return NSAttributedString.Key(rawValue: "URL") } else { return nil } }, - tapAction: { [weak self] _, _ in + tapAction: component.hasPremium ? nil : { [weak self] _, _ in guard let self, let component = self.component else { return } @@ -793,7 +803,7 @@ final class StoryItemSetViewListComponent: Component { } )), environment: {}, - containerSize: CGSize(width: min(320.0, availableSize.width - 16.0 * 2.0), height: 1000.0) + containerSize: CGSize(width: min(fullWidth ? 500.0 : 320.0, availableSize.width - 16.0 * 2.0), height: 1000.0) ) } else { if let premiumFooterText = self.premiumFooterText { @@ -895,27 +905,32 @@ final class StoryItemSetViewListComponent: Component { if self.configuration.listMode == .everyone && (self.query == nil || self.query == "") { if component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) { if emptyButton == nil { - text = component.strings.Story_Views_ViewsExpired + if let views = component.storyItem.views, views.seenCount > 0 { + text = component.strings.Story_Views_ViewsNotRecorded + } else { + text = component.strings.Story_Views_ViewsExpired + } } else { - //TODO:localize - text = "List of viewers isn't available after 24 hours of story expiration.\n\nTo unlock viewers' lists for expired and saved stories, subscribe to [Telegram Premium]()." + text = component.strings.Story_ViewList_PremiumUpgradeText } } else { text = component.strings.Story_Views_NoViews } } else { - //TODO:localize if let query = self.query, !query.isEmpty { - text = "No views found" + text = component.strings.Story_ViewList_EmptyTextSearch } else if self.configuration.listMode == .contacts { - text = "None of your contacts viewed this story." + text = component.strings.Story_ViewList_EmptyTextContacts } else { if component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) { if emptyButton == nil { - text = component.strings.Story_Views_ViewsExpired + if let views = component.storyItem.views, views.seenCount > 0 { + text = component.strings.Story_Views_ViewsNotRecorded + } else { + text = component.strings.Story_Views_ViewsExpired + } } else { - //TODO:localize - text = "List of viewers isn't available after 24 hours of story expiration.\n\nTo unlock viewers' lists for expired and saved stories, subscribe to [Telegram Premium]()." + text = component.strings.Story_ViewList_PremiumUpgradeText } } else { text = component.strings.Story_Views_NoViews @@ -952,7 +967,6 @@ final class StoryItemSetViewListComponent: Component { var emptyButtonSize: CGSize? if let emptyButton { - //TODO:localize emptyButtonSize = emptyButton.update( transition: emptyButtonTransition, component: AnyComponent(ButtonComponent( @@ -964,7 +978,7 @@ final class StoryItemSetViewListComponent: Component { content: AnyComponentWithIdentity( id: AnyHashable(0), component: AnyComponent(ButtonTextContentComponent( - text: "Learn More", + text: component.strings.Story_ViewList_PremiumUpgradeAction, badge: 0, textColor: component.theme.list.itemCheckColors.foregroundColor, badgeBackground: component.theme.list.itemCheckColors.foregroundColor, @@ -1148,10 +1162,9 @@ final class StoryItemSetViewListComponent: Component { let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) var items: [ContextMenuItem] = [] - //TODO:localize let sortMode = self.sortMode - items.append(.action(ContextMenuActionItem(text: "Reactions First", icon: { theme in + items.append(.action(ContextMenuActionItem(text: component.strings.Story_ViewList_ContextSortReactions, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reactions"), color: theme.contextMenu.primaryColor) }, additionalLeftIcon: { theme in if sortMode != .reactionsFirst { @@ -1169,7 +1182,7 @@ final class StoryItemSetViewListComponent: Component { self.state?.updated(transition: .immediate) } }))) - items.append(.action(ContextMenuActionItem(text: "Recent First", icon: { theme in + items.append(.action(ContextMenuActionItem(text: component.strings.Story_ViewList_ContextSortRecent, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Time"), color: theme.contextMenu.primaryColor) }, additionalLeftIcon: { theme in if sortMode != .recentFirst { @@ -1190,9 +1203,8 @@ final class StoryItemSetViewListComponent: Component { items.append(.separator) - //TODO:localize let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil - items.append(.action(ContextMenuActionItem(text: "Choose the order for the list of viewers.", textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction))) + items.append(.action(ContextMenuActionItem(text: component.strings.Story_ViewList_ContextSortInfo, textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction))) let contextItems = ContextController.Items(content: .list(items)) @@ -1233,7 +1245,6 @@ final class StoryItemSetViewListComponent: Component { let visualHeight: CGFloat = max(component.minHeight, component.effectiveHeight) - //TODO:localize let tabSelectorSize = self.tabSelector.update( transition: transition, component: AnyComponent(TabSelectorComponent( @@ -1244,11 +1255,11 @@ final class StoryItemSetViewListComponent: Component { items: [ TabSelectorComponent.Item( id: AnyHashable(ListMode.everyone.rawValue), - title: "All Viewers" + title: component.strings.Story_ViewList_TabTitleAll ), TabSelectorComponent.Item( id: AnyHashable(ListMode.contacts.rawValue), - title: "Contacts" + title: component.strings.Story_ViewList_TabTitleContacts ) ], selectedId: AnyHashable(self.listMode == .everyone ? 0 : 1), @@ -1266,16 +1277,15 @@ final class StoryItemSetViewListComponent: Component { containerSize: CGSize(width: availableSize.width - 10.0 * 2.0, height: 50.0) ) - //TODO:localize let titleText: String if let views = component.storyItem.views, views.seenCount != 0 { if component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) { titleText = component.strings.Story_Footer_Views(Int32(views.seenCount)) } else { - titleText = "Viewers" + titleText = component.strings.Story_ViewList_TitleViewers } } else { - titleText = "No Views" + titleText = component.strings.Story_ViewList_TitleEmpty } let titleSize = self.title.update( @@ -1314,6 +1324,7 @@ final class StoryItemSetViewListComponent: Component { foreground: .white, button: component.theme.rootController.navigationBar.accentTextColor ), + cancel: component.strings.Common_Cancel, placeholder: component.strings.Common_Search, isSearchActive: component.isSearchActive, collapseFraction: 1.0, @@ -1349,7 +1360,7 @@ final class StoryItemSetViewListComponent: Component { if !component.hasPremium, component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) { } else { - if let views = component.storyItem.views { + if let views = component.storyItem.views, views.hasList { if views.seenCount >= 20 || component.context.sharedContext.immediateExperimentalUISettings.storiesExperiment { displayModeSelector = true displaySearchBar = true diff --git a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift index 6241d22dc9..366c1eb2e0 100644 --- a/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryFooterPanelComponent/Sources/StoryFooterPanelComponent.swift @@ -302,7 +302,6 @@ public final class StoryFooterPanelComponent: Component { self.viewStatsButton.isEnabled = viewCount != 0 - //TODO:localize var regularSegments: [AnimatedCountLabelView.Segment] = [] if viewCount != 0 { regularSegments.append(.number(viewCount, NSAttributedString(string: "\(viewCount)", font: Font.regular(15.0), textColor: .white))) @@ -310,11 +309,15 @@ public final class StoryFooterPanelComponent: Component { let viewPart: String if viewCount == 0 { - viewPart = "No Views" - } else if viewCount == 1 { - viewPart = " View" + viewPart = component.strings.Story_Footer_NoViews } else { - viewPart = " Views" + var string = component.strings.Story_Footer_ViewCount(Int32(viewCount)) + if let range = string.range(of: "|") { + if let nextRange = string.range(of: "|", range: range.upperBound ..< string.endIndex) { + string.removeSubrange(string.startIndex ..< nextRange.upperBound) + } + } + viewPart = string } let viewStatsTextLayout = self.viewStatsCountText.update(size: CGSize(width: availableSize.width, height: size.height), segments: regularSegments, transition: isFirstTime ? .immediate : ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)) @@ -419,7 +422,7 @@ public final class StoryFooterPanelComponent: Component { } let minContentX: CGFloat = 16.0 - let maxContentX: CGFloat = floor((availableSize.width - contentWidth) * 0.5) + let maxContentX: CGFloat = (availableSize.width - contentWidth) * 0.5 var contentX: CGFloat = minContentX.interpolate(to: maxContentX, amount: component.expandFraction) let avatarsNodeFrame = CGRect(origin: CGPoint(x: contentX, y: floor((size.height - avatarsSize.height) * 0.5)), size: avatarsSize) diff --git a/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeInfoContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeInfoContentComponent.swift index d3fb424117..38cbd156f7 100644 --- a/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeInfoContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeInfoContentComponent.swift @@ -139,8 +139,7 @@ public final class StoryStealthModeInfoContentComponent: Component { contentHeight += 15.0 let titleString = NSMutableAttributedString() - //TODO:localize - titleString.append(NSAttributedString(string: "Stealth Mode", font: Font.semibold(19.0), textColor: component.theme.list.itemPrimaryTextColor)) + titleString.append(NSAttributedString(string: component.strings.Story_StealthMode_Title, font: Font.semibold(19.0), textColor: component.theme.list.itemPrimaryTextColor)) let imageAttachment = NSTextAttachment() imageAttachment.image = self.iconBackground.image titleString.append(NSAttributedString(attachment: imageAttachment)) @@ -163,13 +162,12 @@ public final class StoryStealthModeInfoContentComponent: Component { contentHeight += titleSize.height contentHeight += 15.0 - //TODO:localize let text: String switch component.mode { case .control: - text = "Turn Stealth Mode on to hide the fact that you viewed peoples' stories from them." + text = component.strings.Story_StealthMode_ControlText case .upgrade: - text = "Subscribe to Telegram Premium to hide the fact that you viewed peoples' stories from them." + text = component.strings.Story_StealthMode_UpgradeText } let mainText = NSMutableAttributedString() mainText.append(parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes( @@ -216,17 +214,16 @@ public final class StoryStealthModeInfoContentComponent: Component { var title: String var text: String } - //TODO:localize let itemDescs: [ItemDesc] = [ ItemDesc( icon: "Stories/StealthModeIntroIconHidePrevious", - title: "Hide Recent Views", - text: "Hide my views in the last **\(timeIntervalString(strings: component.strings, value: component.backwardDuration))**." + title: component.strings.Story_StealthMode_RecentTitle, + text: component.strings.Story_StealthMode_RecentText(timeIntervalString(strings: component.strings, value: component.backwardDuration)).string ), ItemDesc( icon: "Stories/StealthModeIntroIconHideNext", - title: "Hide Next Views", - text: "Hide my views in the next **\(timeIntervalString(strings: component.strings, value: component.forwardDuration))**." + title: component.strings.Story_StealthMode_NextTitle, + text: component.strings.Story_StealthMode_NextText(timeIntervalString(strings: component.strings, value: component.forwardDuration)).string ) ] for i in 0 ..< itemDescs.count { diff --git a/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeSheetScreen.swift b/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeSheetScreen.swift index 24314533f3..7e819bb657 100644 --- a/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeSheetScreen.swift +++ b/submodules/TelegramUI/Components/Stories/StoryStealthModeSheetScreen/Sources/StoryStealthModeSheetScreen.swift @@ -137,7 +137,6 @@ private final class StoryStealthModeSheetContentComponent: Component { toast = ComponentView() self.toast = toast } - //TODO:localize let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white) let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white) let toastSize = toast.update( @@ -149,7 +148,7 @@ private final class StoryStealthModeSheetContentComponent: Component { size: CGSize(width: 32.0, height: 32.0) )), content: AnyComponent(MultilineTextComponent( - text: .markdown(text: "Please wait until the **Stealth Mode** is ready to use again", attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil })), + text: .markdown(text: environment.strings.Story_StealthMode_ToastCooldownText, attributes: MarkdownAttributes(body: body, bold: bold, link: body, linkAttribute: { _ in nil })), maximumNumberOfLines: 0 )) )), @@ -247,13 +246,13 @@ private final class StoryStealthModeSheetContentComponent: Component { switch component.mode { case .control: if remainingCooldownSeconds <= 0 { - buttonText = "Enable Stealth Mode" + buttonText = environment.strings.Story_StealthMode_EnableAction } else { - buttonText = "Available in \(stringForDuration(remainingCooldownSeconds))" + buttonText = environment.strings.Story_StealthMode_CooldownAction(stringForDuration(remainingCooldownSeconds)).string } content = AnyComponentWithIdentity(id: AnyHashable(0 as Int), component: AnyComponent(Text(text: buttonText, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor))) case .upgrade: - buttonText = "Unlock Stealth Mode" + buttonText = environment.strings.Story_StealthMode_UpgradeAction content = AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent( HStack([ AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(Text(text: buttonText, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor))), diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/DownArrow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/DownArrow.imageset/Contents.json new file mode 100644 index 0000000000..c139f982d0 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/DownArrow.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "DownArrow.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/DownArrow.imageset/DownArrow.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/DownArrow.imageset/DownArrow.pdf new file mode 100644 index 0000000000..7e4a5f0bff --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/DownArrow.imageset/DownArrow.pdf @@ -0,0 +1,92 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 0.000000 -1.000000 1.500000 7.822266 cm +0.000000 0.000000 0.000000 scn +-0.586899 2.409164 m +-0.911034 2.085029 -0.911034 1.559502 -0.586899 1.235367 c +-0.262763 0.911232 0.262763 0.911232 0.586899 1.235367 c +-0.586899 2.409164 l +h +4.500000 6.322266 m +5.086899 6.909164 l +4.762764 7.233299 4.237236 7.233299 3.913101 6.909164 c +4.500000 6.322266 l +h +8.413101 1.235367 m +8.737237 0.911232 9.262763 0.911232 9.586899 1.235367 c +9.911034 1.559502 9.911034 2.085029 9.586899 2.409164 c +8.413101 1.235367 l +h +0.586899 1.235367 m +5.086899 5.735367 l +3.913101 6.909164 l +-0.586899 2.409164 l +0.586899 1.235367 l +h +3.913101 5.735367 m +8.413101 1.235367 l +9.586899 2.409164 l +5.086899 6.909164 l +3.913101 5.735367 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 762 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 12.000000 8.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000000852 00000 n +0000000874 00000 n +0000001046 00000 n +0000001120 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1179 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift index 157d82f2c3..57efd1c1b9 100644 --- a/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift +++ b/submodules/TelegramUI/Sources/ChatMessageAnimatedStickerItemNode.swift @@ -1427,7 +1427,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { } } - let updatedImageFrame: CGRect + var updatedImageFrame: CGRect var contextContentFrame: CGRect if let _ = emojiString { updatedImageFrame = imageFrame @@ -1458,8 +1458,11 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView { strongSelf.contextSourceNode.contentRect = contextContentFrame strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect - let animationNodeFrame = updatedContentFrame.insetBy(dx: imageInset, dy: imageInset) - + var animationNodeFrame = updatedContentFrame.insetBy(dx: imageInset, dy: imageInset) + if let telegramFile, telegramFile.isPremiumSticker { + animationNodeFrame = animationNodeFrame.offsetBy(dx: 0.0, dy: 20.0) + } + var file: TelegramMediaFile? if let emojiFile = emojiFile { file = emojiFile diff --git a/submodules/TelegramUI/Sources/NavigateToChatController.swift b/submodules/TelegramUI/Sources/NavigateToChatController.swift index 36b013025e..853d11c761 100644 --- a/submodules/TelegramUI/Sources/NavigateToChatController.swift +++ b/submodules/TelegramUI/Sources/NavigateToChatController.swift @@ -117,6 +117,9 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam if let attachBotStart = params.attachBotStart { controller.presentAttachmentBot(botId: attachBotStart.botId, payload: attachBotStart.payload, justInstalled: attachBotStart.justInstalled) } + if let botAppStart = params.botAppStart, case let .peer(peer) = params.chatLocation { + controller.presentBotApp(botApp: botAppStart.botApp, botPeer: peer, payload: botAppStart.payload) + } params.setupController(controller) found = true break diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 26c6f7e7d9..9fdc0e3ac1 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1869,8 +1869,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) -> ViewController { - return storyMediaPickerController(context: context, getSourceRect: getSourceRect, completion: completion, dismissed: dismissed) + 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 makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController { diff --git a/submodules/TranslateUI/Sources/TranslateScreen.swift b/submodules/TranslateUI/Sources/TranslateScreen.swift index 63a5eca561..ba0f4d1ba7 100644 --- a/submodules/TranslateUI/Sources/TranslateScreen.swift +++ b/submodules/TranslateUI/Sources/TranslateScreen.swift @@ -1075,7 +1075,11 @@ public class TranslateScreen: ViewController { self.component = AnyComponent(component) self.theme = theme - super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: context.sharedContext.currentPresentationData.with { $0 })) + var presentationData = context.sharedContext.currentPresentationData.with { $0 } + if let theme { + presentationData = presentationData.withUpdated(theme: theme) + } + super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData)) } required public init(coder aDecoder: NSCoder) {