mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Merge commit '5884fdff66c79219ea587650503de82d14487d69' into share-v2
This commit is contained in:
commit
1faf0a9d39
@ -9712,6 +9712,7 @@ Sorry for the inconvenience.";
|
|||||||
"Story.Privacy.PostStory" = "Post Story";
|
"Story.Privacy.PostStory" = "Post Story";
|
||||||
|
|
||||||
"Story.Views.ViewsExpired" = "List of viewers becomes unavailable **24 hours** after the story expires.";
|
"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.";
|
"Story.Views.NoViews" = "Nobody has viewed\nyour story yet.";
|
||||||
|
|
||||||
"AutoDownloadSettings.Stories" = "Stories";
|
"AutoDownloadSettings.Stories" = "Stories";
|
||||||
@ -9756,10 +9757,35 @@ Sorry for the inconvenience.";
|
|||||||
"Premium.New" = "NEW";
|
"Premium.New" = "NEW";
|
||||||
|
|
||||||
"MediaEditor.AddGif" = "Add GIF";
|
"MediaEditor.AddGif" = "Add GIF";
|
||||||
|
"MediaEditor.AddLocation" = "Add Location";
|
||||||
|
|
||||||
"Premium.Stories" = "Upgraded Stories";
|
"Premium.Stories" = "Upgraded Stories";
|
||||||
"Premium.StoriesInfo" = "Priority order, stealth mode, permanent views history and more.";
|
"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.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.MaxExpiringStoriesNoPremiumText" = "You have reached the limit of **%@** stories per **24** hours.";
|
||||||
"Premium.MaxExpiringStoriesFinalText" = "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.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.MaxStoriesMonthlyNoPremiumText" = "You have reached the limit of **%@** stories per month.";
|
||||||
"Premium.MaxStoriesMonthlyFinalText" = "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";
|
||||||
|
@ -902,7 +902,7 @@ public protocol SharedAccountContext: AnyObject {
|
|||||||
|
|
||||||
func makeMediaPickerScreen(context: AccountContext, hasSearch: Bool, completion: @escaping (Any) -> Void) -> ViewController
|
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
|
func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController
|
||||||
|
|
||||||
|
@ -131,7 +131,11 @@ public final class ContextActionNode: ASDisplayNode, ContextActionNodeProtocol {
|
|||||||
self.iconNode.displaysAsynchronously = false
|
self.iconNode.displaysAsynchronously = false
|
||||||
self.iconNode.displayWithoutProcessing = true
|
self.iconNode.displayWithoutProcessing = true
|
||||||
self.iconNode.isUserInteractionEnabled = false
|
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)
|
self.iconNode.image = action.icon(presentationData.theme)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,10 +60,14 @@ public enum ContextMenuActionItemFont {
|
|||||||
|
|
||||||
public struct ContextMenuActionItemIconSource {
|
public struct ContextMenuActionItemIconSource {
|
||||||
public let size: CGSize
|
public let size: CGSize
|
||||||
|
public let contentMode: UIView.ContentMode
|
||||||
|
public let cornerRadius: CGFloat
|
||||||
public let signal: Signal<UIImage?, NoError>
|
public let signal: Signal<UIImage?, NoError>
|
||||||
|
|
||||||
public init(size: CGSize, signal: Signal<UIImage?, NoError>) {
|
public init(size: CGSize, contentMode: UIView.ContentMode = .scaleToFill, cornerRadius: CGFloat = 0.0, signal: Signal<UIImage?, NoError>) {
|
||||||
self.size = size
|
self.size = size
|
||||||
|
self.contentMode = contentMode
|
||||||
|
self.cornerRadius = cornerRadius
|
||||||
self.signal = signal
|
self.signal = signal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -287,6 +287,9 @@ private final class ContextControllerActionsListActionItemNode: HighlightTrackin
|
|||||||
let iconSize: CGSize?
|
let iconSize: CGSize?
|
||||||
if let iconSource = self.item.iconSource {
|
if let iconSource = self.item.iconSource {
|
||||||
iconSize = iconSource.size
|
iconSize = iconSource.size
|
||||||
|
self.iconNode.cornerRadius = iconSource.cornerRadius
|
||||||
|
self.iconNode.contentMode = iconSource.contentMode
|
||||||
|
self.iconNode.clipsToBounds = true
|
||||||
if self.iconDisposable == nil {
|
if self.iconDisposable == nil {
|
||||||
self.iconDisposable = (iconSource.signal |> deliverOnMainQueue).start(next: { [weak self] image in
|
self.iconDisposable = (iconSource.signal |> deliverOnMainQueue).start(next: { [weak self] image in
|
||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
|
@ -460,7 +460,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func duplicate(_ entity: DrawingEntity) -> DrawingEntity {
|
func duplicate(_ entity: DrawingEntity) -> DrawingEntity {
|
||||||
let newEntity = entity.duplicate()
|
let newEntity = entity.duplicate(copy: false)
|
||||||
self.prepareNewEntity(newEntity, setup: false, relativeTo: entity)
|
self.prepareNewEntity(newEntity, setup: false, relativeTo: entity)
|
||||||
|
|
||||||
guard let view = makeEntityView(context: self.context, entity: newEntity) else {
|
guard let view = makeEntityView(context: self.context, entity: newEntity) else {
|
||||||
|
@ -171,6 +171,12 @@ public final class DrawingLocationEntityView: DrawingEntityView, UITextViewDeleg
|
|||||||
case .black:
|
case .black:
|
||||||
updatedStyle = .transparent
|
updatedStyle = .transparent
|
||||||
case .transparent:
|
case .transparent:
|
||||||
|
if self.locationEntity.hasCustomColor {
|
||||||
|
updatedStyle = .custom
|
||||||
|
} else {
|
||||||
|
updatedStyle = .white
|
||||||
|
}
|
||||||
|
case .custom:
|
||||||
updatedStyle = .white
|
updatedStyle = .white
|
||||||
case .blur:
|
case .blur:
|
||||||
updatedStyle = .white
|
updatedStyle = .white
|
||||||
@ -217,6 +223,13 @@ public final class DrawingLocationEntityView: DrawingEntityView, UITextViewDeleg
|
|||||||
textColor = .black
|
textColor = .black
|
||||||
case .black, .transparent, .blur:
|
case .black, .transparent, .blur:
|
||||||
textColor = .white
|
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)
|
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.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.2)
|
||||||
self.backgroundView.isHidden = false
|
self.backgroundView.isHidden = false
|
||||||
self.blurredBackgroundView.isHidden = true
|
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:
|
case .blur:
|
||||||
self.textView.textColor = .white
|
self.textView.textColor = .white
|
||||||
self.backgroundView.isHidden = true
|
self.backgroundView.isHidden = true
|
||||||
|
@ -210,7 +210,7 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate
|
|||||||
func beginEditing(accessoryView: UIView?) {
|
func beginEditing(accessoryView: UIView?) {
|
||||||
self._isEditing = true
|
self._isEditing = true
|
||||||
if !self.textEntity.text.string.isEmpty {
|
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
|
previousEntity?.uuid = self.textEntity.uuid
|
||||||
self.previousEntity = previousEntity
|
self.previousEntity = previousEntity
|
||||||
}
|
}
|
||||||
|
@ -2049,7 +2049,7 @@ final class StoryStickersContentView: UIView, EmojiCustomContentView {
|
|||||||
self.locationAction()
|
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 {
|
if useOpaqueTheme {
|
||||||
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlOpaqueSelectionColor.cgColor
|
self.backgroundLayer.backgroundColor = theme.chat.inputMediaPanel.panelContentControlOpaqueSelectionColor.cgColor
|
||||||
self.tintBackgroundLayer.backgroundColor = UIColor.white.cgColor
|
self.tintBackgroundLayer.backgroundColor = UIColor.white.cgColor
|
||||||
@ -2065,7 +2065,7 @@ final class StoryStickersContentView: UIView, EmojiCustomContentView {
|
|||||||
let titleSize = self.title.update(
|
let titleSize = self.title.update(
|
||||||
transition: .immediate,
|
transition: .immediate,
|
||||||
component: AnyComponent(Text(
|
component: AnyComponent(Text(
|
||||||
text: "ADD LOCATION",
|
text: strings.MediaEditor_AddLocation.uppercased(),
|
||||||
font: Font.with(size: 23.0, design: .camera),
|
font: Font.with(size: 23.0, design: .camera),
|
||||||
color: .white
|
color: .white
|
||||||
)),
|
)),
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Contacts
|
import Contacts
|
||||||
import CoreLocation
|
import CoreLocation
|
||||||
|
import MapKit
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
|
|
||||||
public func geocodeLocation(address: String, locale: Locale? = nil) -> Signal<[CLPlacemark]?, NoError> {
|
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<ReverseGeocodedPlacemark?, NoError> {
|
public func reverseGeocodeLocation(latitude: Double, longitude: Double, locale: Locale? = nil) -> Signal<ReverseGeocodedPlacemark?, NoError> {
|
||||||
return Signal { subscriber in
|
return Signal { subscriber in
|
||||||
let geocoder = CLGeocoder()
|
let geocoder = CLGeocoder()
|
||||||
geocoder.reverseGeocodeLocation(CLLocation(latitude: latitude, longitude: longitude), preferredLocale: locale, completionHandler: { placemarks, _ in
|
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
|
let result: ReverseGeocodedPlacemark
|
||||||
if placemark.thoroughfare == nil && placemark.locality == nil && placemark.country == nil {
|
if placemark.thoroughfare == nil && placemark.locality == nil && placemark.country == nil {
|
||||||
result = ReverseGeocodedPlacemark(name: placemark.name, street: placemark.name, city: nil, country: nil, countryCode: nil)
|
result = ReverseGeocodedPlacemark(name: placemark.name, street: placemark.name, city: nil, country: nil, countryCode: nil)
|
||||||
} else {
|
} else {
|
||||||
if placemark.thoroughfare == nil && placemark.locality == nil, let ocean = placemark.ocean {
|
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 {
|
} 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)
|
subscriber.putNext(result)
|
||||||
|
@ -134,8 +134,8 @@ public struct VenueIconArguments: TransformImageCustomArguments {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func venueIcon(engine: TelegramEngine, type: String, background: Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
public func venueIcon(engine: TelegramEngine, type: String, flag: String? = nil, background: Bool) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||||
let isBuiltinIcon = ["", "home", "work"].contains(type)
|
let isBuiltinIcon = ["", "home", "work"].contains(type) || flag != nil
|
||||||
let data: Signal<Data?, NoError> = isBuiltinIcon ? .single(nil) : venueIconData(engine: engine, resource: VenueIconResource(type: type))
|
let data: Signal<Data?, NoError> = isBuiltinIcon ? .single(nil) : venueIconData(engine: engine, resource: VenueIconResource(type: type))
|
||||||
return data |> map { data in
|
return data |> map { data in
|
||||||
return { arguments 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))
|
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)
|
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)
|
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))
|
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 {
|
} else if isBuiltinIcon {
|
||||||
|
@ -279,11 +279,25 @@ final class LocationActionListItemNode: ListViewItemNode {
|
|||||||
case let .venue(venue):
|
case let .venue(venue):
|
||||||
strongSelf.iconNode.isHidden = true
|
strongSelf.iconNode.isHidden = true
|
||||||
strongSelf.venueIconNode.isHidden = false
|
strongSelf.venueIconNode.isHidden = false
|
||||||
strongSelf.venueIconNode.setSignal(venueIcon(engine: item.engine, type: venue.venue?.type ?? "", background: true))
|
|
||||||
|
|
||||||
if venue.venue?.id == "city" {
|
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)
|
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 {
|
if updatedIcon == .stopLiveLocation {
|
||||||
|
@ -150,7 +150,7 @@ private enum LocationPickerEntry: Comparable, Identifiable {
|
|||||||
case let .city(_, title, subtitle, _, _, _, coordinate, name, countryCode):
|
case let .city(_, title, subtitle, _, _, _, coordinate, name, countryCode):
|
||||||
let icon: LocationActionListItemIcon
|
let icon: LocationActionListItemIcon
|
||||||
if let name {
|
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 {
|
} else {
|
||||||
icon = .location
|
icon = .location
|
||||||
}
|
}
|
||||||
@ -583,20 +583,28 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
|
|||||||
switch strongSelf.mode {
|
switch strongSelf.mode {
|
||||||
case .share:
|
case .share:
|
||||||
if source == .story {
|
if source == .story {
|
||||||
title = "Add This Location"
|
title = presentationData.strings.Location_AddThisLocation
|
||||||
} else {
|
} else {
|
||||||
title = presentationData.strings.Map_SendThisLocation
|
title = presentationData.strings.Map_SendThisLocation
|
||||||
}
|
}
|
||||||
case .pick:
|
case .pick:
|
||||||
title = presentationData.strings.Map_SetThisLocation
|
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:
|
case .selecting:
|
||||||
let title: String
|
let title: String
|
||||||
switch strongSelf.mode {
|
switch strongSelf.mode {
|
||||||
case .share:
|
case .share:
|
||||||
if source == .story {
|
if source == .story {
|
||||||
title = "Add This Location"
|
title = presentationData.strings.Location_AddThisLocation
|
||||||
} else {
|
} else {
|
||||||
title = presentationData.strings.Map_SendThisLocation
|
title = presentationData.strings.Map_SendThisLocation
|
||||||
}
|
}
|
||||||
@ -620,10 +628,10 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
|
|||||||
case .share:
|
case .share:
|
||||||
if source == .story {
|
if source == .story {
|
||||||
if let initialLocation = strongSelf.controller?.initialLocation {
|
if let initialLocation = strongSelf.controller?.initialLocation {
|
||||||
title = "Add This Location"
|
title = presentationData.strings.Location_AddThisLocation
|
||||||
coordinate = initialLocation
|
coordinate = initialLocation
|
||||||
} else {
|
} else {
|
||||||
title = "Add My Current Location"
|
title = presentationData.strings.Location_AddMyLocation
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
title = presentationData.strings.Map_SendMyCurrentLocation
|
title = presentationData.strings.Map_SendMyCurrentLocation
|
||||||
@ -633,10 +641,10 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
|
|||||||
}
|
}
|
||||||
if source == .story {
|
if source == .story {
|
||||||
if state.city != "" {
|
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 != "" {
|
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 {
|
} 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))
|
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 cityName: String?
|
||||||
var streetName: String?
|
var streetName: String?
|
||||||
let countryCode = placemark?.countryCode
|
let countryCode = placemark?.countryCode
|
||||||
if let city = placemark?.city, let countryCode = placemark?.countryCode {
|
if let city = placemark?.city {
|
||||||
|
if let countryCode = placemark?.countryCode {
|
||||||
cityName = "\(city), \(displayCountryName(countryCode, locale: locale))"
|
cityName = "\(city), \(displayCountryName(countryCode, locale: locale))"
|
||||||
|
} else {
|
||||||
|
cityName = city
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
cityName = ""
|
cityName = ""
|
||||||
}
|
}
|
||||||
@ -809,7 +821,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
|
|||||||
streetName = ""
|
streetName = ""
|
||||||
}
|
}
|
||||||
if streetName == "" && cityName == "" {
|
if streetName == "" && cityName == "" {
|
||||||
streetName = "Location"
|
streetName = presentationData.strings.Location_TypeLocation
|
||||||
}
|
}
|
||||||
strongSelf.updateState { state in
|
strongSelf.updateState { state in
|
||||||
var state = state
|
var state = state
|
||||||
@ -835,8 +847,12 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
|
|||||||
var cityName: String?
|
var cityName: String?
|
||||||
var streetName: String?
|
var streetName: String?
|
||||||
let countryCode = placemark?.countryCode
|
let countryCode = placemark?.countryCode
|
||||||
if let city = placemark?.city, let countryCode = placemark?.countryCode {
|
if let city = placemark?.city {
|
||||||
|
if let countryCode = placemark?.countryCode {
|
||||||
cityName = "\(city), \(displayCountryName(countryCode, locale: locale))"
|
cityName = "\(city), \(displayCountryName(countryCode, locale: locale))"
|
||||||
|
} else {
|
||||||
|
cityName = city
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
cityName = ""
|
cityName = ""
|
||||||
}
|
}
|
||||||
@ -854,7 +870,7 @@ final class LocationPickerControllerNode: ViewControllerTracingNode, CLLocationM
|
|||||||
streetName = ""
|
streetName = ""
|
||||||
}
|
}
|
||||||
if streetName == "" && cityName == "" {
|
if streetName == "" && cityName == "" {
|
||||||
streetName = "Location"
|
streetName = presentationData.strings.Location_TypeLocation
|
||||||
}
|
}
|
||||||
strongSelf.updateState { state in
|
strongSelf.updateState { state in
|
||||||
var state = state
|
var state = state
|
||||||
|
@ -291,6 +291,7 @@ final class LocationViewControllerNode: ViewControllerTracingNode, CLLocationMan
|
|||||||
var eta: Signal<(ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime), NoError> = .single((.calculating, .calculating, .calculating))
|
var eta: Signal<(ExpectedTravelTime, ExpectedTravelTime, ExpectedTravelTime), NoError> = .single((.calculating, .calculating, .calculating))
|
||||||
var address: Signal<String?, NoError> = .single(nil)
|
var address: Signal<String?, NoError> = .single(nil)
|
||||||
|
|
||||||
|
let locale = localeWithStrings(presentationData.strings)
|
||||||
if let location = getLocation(from: subject), location.liveBroadcastingTimeout == nil {
|
if let location = getLocation(from: subject), location.liveBroadcastingTimeout == nil {
|
||||||
eta = .single((.calculating, .calculating, .calculating))
|
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))
|
|> 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 {
|
} else {
|
||||||
address = .single(nil)
|
address = .single(nil)
|
||||||
|> then(
|
|> then(
|
||||||
reverseGeocodeLocation(latitude: location.latitude, longitude: location.longitude)
|
reverseGeocodeLocation(latitude: location.latitude, longitude: location.longitude, locale: locale)
|
||||||
|> map { placemark -> String? in
|
|> map { placemark -> String? in
|
||||||
return placemark?.compactDisplayAddress ?? ""
|
return placemark?.compactDisplayAddress ?? ""
|
||||||
}
|
}
|
||||||
|
@ -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<Int>()
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -309,6 +309,9 @@ final class MediaPickerGridItemNode: GridItemNode {
|
|||||||
|
|
||||||
self.progressDisposable.set(nil)
|
self.progressDisposable.set(nil)
|
||||||
self.updateProgress(nil, animated: false)
|
self.updateProgress(nil, animated: false)
|
||||||
|
|
||||||
|
self.backgroundNode.image = nil
|
||||||
|
self.imageNode.contentMode = .scaleAspectFill
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.draftNode.supernode == nil {
|
if self.draftNode.supernode == nil {
|
||||||
@ -403,6 +406,7 @@ final class MediaPickerGridItemNode: GridItemNode {
|
|||||||
if asset.localIdentifier == self.currentAsset?.localIdentifier {
|
if asset.localIdentifier == self.currentAsset?.localIdentifier {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
self.backgroundNode.image = nil
|
||||||
|
|
||||||
self.progressDisposable.set(
|
self.progressDisposable.set(
|
||||||
(interaction.downloadManager.downloadProgress(identifier: asset.localIdentifier)
|
(interaction.downloadManager.downloadProgress(identifier: asset.localIdentifier)
|
||||||
|
@ -81,6 +81,19 @@ private struct MediaPickerGridTransaction {
|
|||||||
|
|
||||||
self.scrollToItem = scrollToItem
|
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 {
|
struct Month: Equatable {
|
||||||
@ -186,6 +199,8 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
public var isContainerPanning: () -> Bool = { return false }
|
public var isContainerPanning: () -> Bool = { return false }
|
||||||
public var isContainerExpanded: () -> Bool = { return false }
|
public var isContainerExpanded: () -> Bool = { return false }
|
||||||
|
|
||||||
|
private let selectedCollection = Promise<PHAssetCollection?>(nil)
|
||||||
|
|
||||||
var dismissAll: () -> Void = { }
|
var dismissAll: () -> Void = { }
|
||||||
|
|
||||||
private class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate {
|
private class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate {
|
||||||
@ -284,6 +299,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
self.containerNode.addSubnode(self.gridNode)
|
self.containerNode.addSubnode(self.gridNode)
|
||||||
self.containerNode.addSubnode(self.scrollingArea)
|
self.containerNode.addSubnode(self.scrollingArea)
|
||||||
|
|
||||||
|
let selectedCollection = controller.selectedCollection.get()
|
||||||
let preloadPromise = self.preloadPromise
|
let preloadPromise = self.preloadPromise
|
||||||
let updatedState: Signal<State, NoError>
|
let updatedState: Signal<State, NoError>
|
||||||
switch controller.subject {
|
switch controller.subject {
|
||||||
@ -301,10 +317,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
} else if [.restricted, .denied].contains(mediaAccess) {
|
} else if [.restricted, .denied].contains(mediaAccess) {
|
||||||
return .single(.noAccess(cameraAccess: cameraAccess))
|
return .single(.noAccess(cameraAccess: cameraAccess))
|
||||||
} else {
|
} else {
|
||||||
if let collection = collection {
|
return selectedCollection
|
||||||
|
|> mapToSignal { selectedCollection in
|
||||||
|
let collection = selectedCollection ?? collection
|
||||||
|
if let collection {
|
||||||
return combineLatest(mediaAssetsContext.fetchAssets(collection), preloadPromise.get())
|
return combineLatest(mediaAssetsContext.fetchAssets(collection), preloadPromise.get())
|
||||||
|> map { fetchResult, preload in
|
|> map { fetchResult, preload in
|
||||||
return .assets(fetchResult: fetchResult, preload: preload, drafts: [], mediaAccess: mediaAccess, cameraAccess: cameraAccess)
|
return .assets(fetchResult: fetchResult, preload: preload, drafts: [], mediaAccess: mediaAccess, cameraAccess: selectedCollection != nil ? nil : cameraAccess)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return combineLatest(mediaAssetsContext.recentAssets(), preloadPromise.get(), drafts)
|
return combineLatest(mediaAssetsContext.recentAssets(), preloadPromise.get(), drafts)
|
||||||
@ -314,6 +333,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
case let .media(media):
|
case let .media(media):
|
||||||
updatedState = .single(.media(media))
|
updatedState = .single(.media(media))
|
||||||
}
|
}
|
||||||
@ -578,6 +598,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileprivate var resetOnUpdate = false
|
||||||
private func updateState(_ state: State) {
|
private func updateState(_ state: State) {
|
||||||
guard let controller = self.controller, let interaction = controller.interaction else {
|
guard let controller = self.controller, let interaction = controller.interaction else {
|
||||||
return
|
return
|
||||||
@ -652,6 +673,72 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
self.requestedCameraAccess = true
|
self.requestedCameraAccess = true
|
||||||
self.mediaAssetsContext.requestCameraAccess()
|
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 {
|
} else if case .notDetermined = mediaAccess, !self.requestedMediaAccess {
|
||||||
self.requestedMediaAccess = true
|
self.requestedMediaAccess = true
|
||||||
self.mediaAssetsContext.requestMediaAccess()
|
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
|
self.currentEntries = entries
|
||||||
|
|
||||||
var scrollToItem: GridNodeScrollToItem?
|
var scrollToItem: GridNodeScrollToItem?
|
||||||
@ -685,6 +781,10 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
self.updateNavigation(transition: .immediate)
|
self.updateNavigation(transition: .immediate)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func resetItems() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private func updateSelectionState(animated: Bool = false) {
|
private func updateSelectionState(animated: Bool = false) {
|
||||||
self.gridNode.forEachItemNode { itemNode in
|
self.gridNode.forEachItemNode { itemNode in
|
||||||
if let itemNode = itemNode as? MediaPickerGridItemNode {
|
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
|
self.titleView.title = collection.localizedTitle ?? presentationData.strings.Attachment_Gallery
|
||||||
} else {
|
} else {
|
||||||
switch mode {
|
switch mode {
|
||||||
case .default, .story:
|
case .default:
|
||||||
self.titleView.title = presentationData.strings.Attachment_Gallery
|
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:
|
case .wallpaper:
|
||||||
self.titleView.title = presentationData.strings.Conversation_Theme_ChooseWallpaperTitle
|
self.titleView.title = presentationData.strings.Conversation_Theme_ChooseWallpaperTitle
|
||||||
case .addImage:
|
case .addImage:
|
||||||
@ -1528,6 +1632,12 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.titleView.action = { [weak self] in
|
||||||
|
if let self {
|
||||||
|
self.presentGroupsMenu()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.navigationItem.titleView = self.titleView
|
self.navigationItem.titleView = self.titleView
|
||||||
|
|
||||||
if case let .assets(collection, mode) = self.subject, mode != .default {
|
if case let .assets(collection, mode) = self.subject, mode != .default {
|
||||||
@ -1703,6 +1813,59 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
|||||||
self.controllerNode.closeGalleryController()
|
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 weak var undoOverlayController: UndoOverlayController?
|
||||||
private func showSelectionUndo(item: TGMediaSelectableItem) {
|
private func showSelectionUndo(item: TGMediaSelectableItem) {
|
||||||
let scale = min(2.0, UIScreenScale)
|
let scale = min(2.0, UIScreenScale)
|
||||||
@ -2336,7 +2499,8 @@ public func storyMediaPickerController(
|
|||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
getSourceRect: @escaping () -> CGRect,
|
getSourceRect: @escaping () -> CGRect,
|
||||||
completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void,
|
completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void,
|
||||||
dismissed: @escaping () -> Void
|
dismissed: @escaping () -> Void,
|
||||||
|
groupsPresented: @escaping () -> Void
|
||||||
) -> ViewController {
|
) -> ViewController {
|
||||||
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme)
|
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme)
|
||||||
let updatedPresentationData: (PresentationData, Signal<PresentationData, NoError>) = (presentationData, .single(presentationData))
|
let updatedPresentationData: (PresentationData, Signal<PresentationData, NoError>) = (presentationData, .single(presentationData))
|
||||||
@ -2347,6 +2511,7 @@ public func storyMediaPickerController(
|
|||||||
controller.getSourceRect = getSourceRect
|
controller.getSourceRect = getSourceRect
|
||||||
controller.requestController = { _, present in
|
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)
|
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
|
mediaPickerController.customSelection = { controller, result in
|
||||||
if let result = result as? MediaEditorDraft {
|
if let result = result as? MediaEditorDraft {
|
||||||
controller.updateHiddenMediaId(result.path)
|
controller.updateHiddenMediaId(result.path)
|
||||||
|
@ -4,9 +4,13 @@ import AsyncDisplayKit
|
|||||||
import Display
|
import Display
|
||||||
import TelegramPresentationData
|
import TelegramPresentationData
|
||||||
import SegmentedControlNode
|
import SegmentedControlNode
|
||||||
|
import ContextUI
|
||||||
|
|
||||||
final class MediaPickerTitleView: UIView {
|
final class MediaPickerTitleView: UIView {
|
||||||
|
let contextSourceNode: ContextReferenceContentNode
|
||||||
|
private let buttonNode: HighlightTrackingButtonNode
|
||||||
private let titleNode: ImmediateTextNode
|
private let titleNode: ImmediateTextNode
|
||||||
|
private let arrowNode: ASImageNode
|
||||||
private let segmentedControlNode: SegmentedControlNode
|
private let segmentedControlNode: SegmentedControlNode
|
||||||
|
|
||||||
public var theme: PresentationTheme {
|
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 {
|
public var segmentsHidden = true {
|
||||||
didSet {
|
didSet {
|
||||||
if self.segmentsHidden != oldValue {
|
if self.segmentsHidden != oldValue {
|
||||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.21, curve: .easeInOut)
|
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.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)
|
transition.updateAlpha(node: self.segmentedControlNode, alpha: self.segmentsHidden ? 0.0 : 1.0)
|
||||||
self.segmentedControlNode.isUserInteractionEnabled = !self.segmentsHidden
|
self.segmentedControlNode.isUserInteractionEnabled = !self.segmentsHidden
|
||||||
}
|
}
|
||||||
@ -55,14 +73,23 @@ final class MediaPickerTitleView: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public var indexUpdated: ((Int) -> Void)?
|
public var indexUpdated: ((Int) -> Void)?
|
||||||
|
public var action: () -> Void = {}
|
||||||
|
|
||||||
public init(theme: PresentationTheme, segments: [String], selectedIndex: Int) {
|
public init(theme: PresentationTheme, segments: [String], selectedIndex: Int) {
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.segments = segments
|
self.segments = segments
|
||||||
|
|
||||||
|
self.contextSourceNode = ContextReferenceContentNode()
|
||||||
|
self.buttonNode = HighlightTrackingButtonNode()
|
||||||
|
|
||||||
self.titleNode = ImmediateTextNode()
|
self.titleNode = ImmediateTextNode()
|
||||||
self.titleNode.displaysAsynchronously = false
|
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 = SegmentedControlNode(theme: SegmentedControlTheme(theme: theme), items: segments.map { SegmentedControlItem(title: $0) }, selectedIndex: selectedIndex)
|
||||||
self.segmentedControlNode.alpha = 0.0
|
self.segmentedControlNode.alpha = 0.0
|
||||||
self.segmentedControlNode.isUserInteractionEnabled = false
|
self.segmentedControlNode.isUserInteractionEnabled = false
|
||||||
@ -73,8 +100,26 @@ final class MediaPickerTitleView: UIView {
|
|||||||
self?.indexUpdated?(index)
|
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.titleNode)
|
||||||
|
self.addSubnode(self.arrowNode)
|
||||||
|
self.addSubnode(self.buttonNode)
|
||||||
self.addSubnode(self.segmentedControlNode)
|
self.addSubnode(self.segmentedControlNode)
|
||||||
|
|
||||||
|
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||||
}
|
}
|
||||||
|
|
||||||
required public init?(coder aDecoder: NSCoder) {
|
required public init?(coder aDecoder: NSCoder) {
|
||||||
@ -85,10 +130,21 @@ final class MediaPickerTitleView: UIView {
|
|||||||
super.layoutSubviews()
|
super.layoutSubviews()
|
||||||
|
|
||||||
let size = self.bounds.size
|
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)
|
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)
|
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))
|
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)
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -342,7 +342,8 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent {
|
|||||||
UIColor(rgb: 0x548DFF),
|
UIColor(rgb: 0x548DFF),
|
||||||
UIColor(rgb: 0x54A3FF),
|
UIColor(rgb: 0x54A3FF),
|
||||||
UIColor(rgb: 0x54bdff),
|
UIColor(rgb: 0x54bdff),
|
||||||
UIColor(rgb: 0x71c8ff)
|
UIColor(rgb: 0x71c8ff),
|
||||||
|
UIColor(rgb: 0xa0daff)
|
||||||
]
|
]
|
||||||
|
|
||||||
i = 0
|
i = 0
|
||||||
|
@ -360,7 +360,8 @@ public enum PremiumPerk: CaseIterable {
|
|||||||
.appIcons,
|
.appIcons,
|
||||||
.animatedEmoji,
|
.animatedEmoji,
|
||||||
.emojiStatus,
|
.emojiStatus,
|
||||||
.translation
|
.translation,
|
||||||
|
.stories
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -522,6 +523,7 @@ struct PremiumIntroConfiguration {
|
|||||||
.doubleLimits,
|
.doubleLimits,
|
||||||
.moreUpload,
|
.moreUpload,
|
||||||
.fasterDownload,
|
.fasterDownload,
|
||||||
|
.translation,
|
||||||
.voiceToText,
|
.voiceToText,
|
||||||
.noAds,
|
.noAds,
|
||||||
.emojiStatus,
|
.emojiStatus,
|
||||||
@ -1614,17 +1616,6 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
)
|
)
|
||||||
context.add(text
|
context.add(text
|
||||||
.position(CGPoint(x: size.width / 2.0, y: size.height + text.size.height / 2.0))
|
.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 += text.size.height
|
||||||
size.height += 21.0
|
size.height += 21.0
|
||||||
@ -1643,7 +1634,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent {
|
|||||||
UIColor(rgb: 0x548DFF),
|
UIColor(rgb: 0x548DFF),
|
||||||
UIColor(rgb: 0x54A3FF),
|
UIColor(rgb: 0x54A3FF),
|
||||||
UIColor(rgb: 0x54bdff),
|
UIColor(rgb: 0x54bdff),
|
||||||
UIColor(rgb: 0x71c8ff)
|
UIColor(rgb: 0x71c8ff),
|
||||||
|
UIColor(rgb: 0xa0daff)
|
||||||
]
|
]
|
||||||
|
|
||||||
let accountContext = context.component.context
|
let accountContext = context.component.context
|
||||||
|
@ -309,7 +309,7 @@ private final class StoriesListComponent: CombinedComponent {
|
|||||||
|
|
||||||
return { context in
|
return { context in
|
||||||
let theme = context.component.theme
|
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 = [
|
let colors = [
|
||||||
UIColor(rgb: 0x0275f3),
|
UIColor(rgb: 0x0275f3),
|
||||||
@ -343,9 +343,9 @@ private final class StoriesListComponent: CombinedComponent {
|
|||||||
AnyComponentWithIdentity(
|
AnyComponentWithIdentity(
|
||||||
id: "order",
|
id: "order",
|
||||||
component: AnyComponent(ParagraphComponent(
|
component: AnyComponent(ParagraphComponent(
|
||||||
title: "Priority Order",
|
title: strings.Premium_Stories_Order_Title,
|
||||||
titleColor: titleColor,
|
titleColor: titleColor,
|
||||||
text: "Get more views as your stories are always displayed first.",
|
text: strings.Premium_Stories_Order_Text,
|
||||||
textColor: textColor,
|
textColor: textColor,
|
||||||
iconName: "Premium/Stories/Order",
|
iconName: "Premium/Stories/Order",
|
||||||
iconColor: colors[0]
|
iconColor: colors[0]
|
||||||
@ -357,9 +357,9 @@ private final class StoriesListComponent: CombinedComponent {
|
|||||||
AnyComponentWithIdentity(
|
AnyComponentWithIdentity(
|
||||||
id: "stealth",
|
id: "stealth",
|
||||||
component: AnyComponent(ParagraphComponent(
|
component: AnyComponent(ParagraphComponent(
|
||||||
title: "Stealth Mode",
|
title: strings.Premium_Stories_Stealth_Title,
|
||||||
titleColor: titleColor,
|
titleColor: titleColor,
|
||||||
text: "Hide the fact that you viewed other people's stories.",
|
text: strings.Premium_Stories_Stealth_Text,
|
||||||
textColor: textColor,
|
textColor: textColor,
|
||||||
iconName: "Premium/Stories/Stealth",
|
iconName: "Premium/Stories/Stealth",
|
||||||
iconColor: colors[1]
|
iconColor: colors[1]
|
||||||
@ -371,9 +371,9 @@ private final class StoriesListComponent: CombinedComponent {
|
|||||||
AnyComponentWithIdentity(
|
AnyComponentWithIdentity(
|
||||||
id: "views",
|
id: "views",
|
||||||
component: AnyComponent(ParagraphComponent(
|
component: AnyComponent(ParagraphComponent(
|
||||||
title: "Permanent Views History",
|
title: strings.Premium_Stories_Views_Title,
|
||||||
titleColor: titleColor,
|
titleColor: titleColor,
|
||||||
text: "Check who opens your stories — even after they expire.",
|
text: strings.Premium_Stories_Views_Text,
|
||||||
textColor: textColor,
|
textColor: textColor,
|
||||||
iconName: "Premium/Stories/Views",
|
iconName: "Premium/Stories/Views",
|
||||||
iconColor: colors[2]
|
iconColor: colors[2]
|
||||||
@ -385,9 +385,9 @@ private final class StoriesListComponent: CombinedComponent {
|
|||||||
AnyComponentWithIdentity(
|
AnyComponentWithIdentity(
|
||||||
id: "expiration",
|
id: "expiration",
|
||||||
component: AnyComponent(ParagraphComponent(
|
component: AnyComponent(ParagraphComponent(
|
||||||
title: "Expiration Durations",
|
title: strings.Premium_Stories_Expiration_Title,
|
||||||
titleColor: titleColor,
|
titleColor: titleColor,
|
||||||
text: "Set custom expiration durations like 6 or 48 hours for your stories.",
|
text: strings.Premium_Stories_Expiration_Text,
|
||||||
textColor: textColor,
|
textColor: textColor,
|
||||||
iconName: "Premium/Stories/Expire",
|
iconName: "Premium/Stories/Expire",
|
||||||
iconColor: colors[3]
|
iconColor: colors[3]
|
||||||
@ -399,9 +399,9 @@ private final class StoriesListComponent: CombinedComponent {
|
|||||||
AnyComponentWithIdentity(
|
AnyComponentWithIdentity(
|
||||||
id: "save",
|
id: "save",
|
||||||
component: AnyComponent(ParagraphComponent(
|
component: AnyComponent(ParagraphComponent(
|
||||||
title: "Save Stories to Gallery",
|
title: strings.Premium_Stories_Save_Title,
|
||||||
titleColor: titleColor,
|
titleColor: titleColor,
|
||||||
text: "Save other people's unprotected stories to your Gallery.",
|
text: strings.Premium_Stories_Save_Text,
|
||||||
textColor: textColor,
|
textColor: textColor,
|
||||||
iconName: "Premium/Stories/Save",
|
iconName: "Premium/Stories/Save",
|
||||||
iconColor: colors[4]
|
iconColor: colors[4]
|
||||||
@ -413,9 +413,9 @@ private final class StoriesListComponent: CombinedComponent {
|
|||||||
AnyComponentWithIdentity(
|
AnyComponentWithIdentity(
|
||||||
id: "captions",
|
id: "captions",
|
||||||
component: AnyComponent(ParagraphComponent(
|
component: AnyComponent(ParagraphComponent(
|
||||||
title: "Longer Captions",
|
title: strings.Premium_Stories_Captions_Title,
|
||||||
titleColor: titleColor,
|
titleColor: titleColor,
|
||||||
text: "Add ten times longer captions to your stories.",
|
text: strings.Premium_Stories_Captions_Text,
|
||||||
textColor: textColor,
|
textColor: textColor,
|
||||||
iconName: "Premium/Stories/Caption",
|
iconName: "Premium/Stories/Caption",
|
||||||
iconColor: colors[5]
|
iconColor: colors[5]
|
||||||
@ -427,9 +427,9 @@ private final class StoriesListComponent: CombinedComponent {
|
|||||||
AnyComponentWithIdentity(
|
AnyComponentWithIdentity(
|
||||||
id: "format",
|
id: "format",
|
||||||
component: AnyComponent(ParagraphComponent(
|
component: AnyComponent(ParagraphComponent(
|
||||||
title: "Links and Formatting",
|
title: strings.Premium_Stories_Format_Title,
|
||||||
titleColor: titleColor,
|
titleColor: titleColor,
|
||||||
text: "Add links and formatting in captions to your stories.",
|
text: strings.Premium_Stories_Format_Text,
|
||||||
textColor: textColor,
|
textColor: textColor,
|
||||||
iconName: "Premium/Stories/Format",
|
iconName: "Premium/Stories/Format",
|
||||||
iconColor: colors[6]
|
iconColor: colors[6]
|
||||||
@ -581,7 +581,7 @@ final class StoriesPageComponent: CombinedComponent {
|
|||||||
state.isDisplaying = environment.isDisplaying
|
state.isDisplaying = environment.isDisplaying
|
||||||
|
|
||||||
let theme = context.component.theme
|
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
|
let topInset: CGFloat = 56.0
|
||||||
|
|
||||||
@ -641,7 +641,7 @@ final class StoriesPageComponent: CombinedComponent {
|
|||||||
|
|
||||||
let title = title.update(
|
let title = title.update(
|
||||||
component: MultilineTextComponent(
|
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,
|
horizontalAlignment: .center,
|
||||||
truncationType: .end,
|
truncationType: .end,
|
||||||
maximumNumberOfLines: 1
|
maximumNumberOfLines: 1
|
||||||
@ -652,7 +652,7 @@ final class StoriesPageComponent: CombinedComponent {
|
|||||||
|
|
||||||
let secondaryTitle = secondaryTitle.update(
|
let secondaryTitle = secondaryTitle.update(
|
||||||
component: MultilineTextComponent(
|
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,
|
horizontalAlignment: .center,
|
||||||
truncationType: .end,
|
truncationType: .end,
|
||||||
maximumNumberOfLines: 1
|
maximumNumberOfLines: 1
|
||||||
|
@ -233,7 +233,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
private var animationHideNode: Bool = false
|
private var animationHideNode: Bool = false
|
||||||
|
|
||||||
public var displayTail: Bool = true
|
public var displayTail: Bool = true
|
||||||
public var forceTailToRight: Bool = true
|
public var forceTailToRight: Bool = false
|
||||||
|
|
||||||
private var didAnimateIn: Bool = false
|
private var didAnimateIn: Bool = false
|
||||||
public private(set) var isAnimatingOut: Bool = false
|
public private(set) var isAnimatingOut: Bool = false
|
||||||
|
@ -708,7 +708,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili
|
|||||||
}
|
}
|
||||||
let upload: Signal<MultipartUploadResult?, PendingMessageUploadError> = .single(nil)
|
let upload: Signal<MultipartUploadResult?, PendingMessageUploadError> = .single(nil)
|
||||||
|> then(
|
|> 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 }
|
|> mapError { _ -> PendingMessageUploadError in return .generic }
|
||||||
|> map(Optional.init)
|
|> map(Optional.init)
|
||||||
)
|
)
|
||||||
|
@ -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<MultipartUploadResult, MultipartUploadError> {
|
func upload(network: Network, postbox: Postbox, source: MultipartUploadSource, encrypt: Bool, tag: MediaResourceFetchTag?, hintFileSize: Int64?, hintFileIsLarge: Bool, forceNoBigParts: Bool) -> Signal<MultipartUploadResult, MultipartUploadError> {
|
||||||
let queue = self.queue
|
let queue = self.queue
|
||||||
return Signal { [weak self] subscriber in
|
return Signal { [weak self] subscriber in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
@ -93,7 +93,7 @@ private final class MessageMediaPreuploadManagerContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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)
|
subscriber.putNext(next)
|
||||||
}, error: { error in
|
}, error: { error in
|
||||||
subscriber.putError(error)
|
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<MultipartUploadResult, MultipartUploadError> {
|
func upload(network: Network, postbox: Postbox, source: MultipartUploadSource, encrypt: Bool, tag: MediaResourceFetchTag?, hintFileSize: Int64?, hintFileIsLarge: Bool, forceNoBigParts: Bool) -> Signal<MultipartUploadResult, MultipartUploadError> {
|
||||||
return Signal<Signal<MultipartUploadResult, MultipartUploadError>, MultipartUploadError> { subscriber in
|
return Signal<Signal<MultipartUploadResult, MultipartUploadError>, MultipartUploadError> { subscriber in
|
||||||
self.impl.with { context 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()
|
subscriber.putCompletion()
|
||||||
}
|
}
|
||||||
return EmptyDisposable
|
return EmptyDisposable
|
||||||
|
@ -335,7 +335,7 @@ public final class EngineStoryViewListContext {
|
|||||||
mediaAreas: item.mediaAreas,
|
mediaAreas: item.mediaAreas,
|
||||||
text: item.text,
|
text: item.text,
|
||||||
entities: item.entities,
|
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,
|
privacy: item.privacy,
|
||||||
isPinned: item.isPinned,
|
isPinned: item.isPinned,
|
||||||
isExpired: item.isExpired,
|
isExpired: item.isExpired,
|
||||||
@ -364,7 +364,7 @@ public final class EngineStoryViewListContext {
|
|||||||
mediaAreas: item.mediaAreas,
|
mediaAreas: item.mediaAreas,
|
||||||
text: item.text,
|
text: item.text,
|
||||||
entities: item.entities,
|
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,
|
privacy: item.privacy,
|
||||||
isPinned: item.isPinned,
|
isPinned: item.isPinned,
|
||||||
isExpired: item.isExpired,
|
isExpired: item.isExpired,
|
||||||
|
@ -43,16 +43,19 @@ public enum Stories {
|
|||||||
case seenCount = "seenCount"
|
case seenCount = "seenCount"
|
||||||
case reactedCount = "reactedCount"
|
case reactedCount = "reactedCount"
|
||||||
case seenPeerIds = "seenPeerIds"
|
case seenPeerIds = "seenPeerIds"
|
||||||
|
case hasList = "hasList"
|
||||||
}
|
}
|
||||||
|
|
||||||
public var seenCount: Int
|
public var seenCount: Int
|
||||||
public var reactedCount: Int
|
public var reactedCount: Int
|
||||||
public var seenPeerIds: [PeerId]
|
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.seenCount = seenCount
|
||||||
self.reactedCount = reactedCount
|
self.reactedCount = reactedCount
|
||||||
self.seenPeerIds = seenPeerIds
|
self.seenPeerIds = seenPeerIds
|
||||||
|
self.hasList = hasList
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
@ -61,6 +64,7 @@ public enum Stories {
|
|||||||
self.seenCount = Int(try container.decode(Int32.self, forKey: .seenCount))
|
self.seenCount = Int(try container.decode(Int32.self, forKey: .seenCount))
|
||||||
self.reactedCount = Int(try container.decodeIfPresent(Int32.self, forKey: .reactedCount) ?? 0)
|
self.reactedCount = Int(try container.decodeIfPresent(Int32.self, forKey: .reactedCount) ?? 0)
|
||||||
self.seenPeerIds = try container.decode([Int64].self, forKey: .seenPeerIds).map(PeerId.init)
|
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 {
|
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.seenCount), forKey: .seenCount)
|
||||||
try container.encode(Int32(clamping: self.reactedCount), forKey: .reactedCount)
|
try container.encode(Int32(clamping: self.reactedCount), forKey: .reactedCount)
|
||||||
try container.encode(self.seenPeerIds.map { $0.toInt64() }, forKey: .seenPeerIds)
|
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 {
|
extension Stories.Item.Views {
|
||||||
init(apiViews: Api.StoryViews) {
|
init(apiViews: Api.StoryViews) {
|
||||||
switch apiViews {
|
switch apiViews {
|
||||||
case let .storyViews(_, viewsCount, reactionsCount, recentViewers):
|
case let .storyViews(flags, viewsCount, reactionsCount, recentViewers):
|
||||||
|
let hasList = (flags & (1 << 1)) != 0
|
||||||
var seenPeerIds: [PeerId] = []
|
var seenPeerIds: [PeerId] = []
|
||||||
if let recentViewers = recentViewers {
|
if let recentViewers = recentViewers {
|
||||||
seenPeerIds = recentViewers.map { PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value($0)) }
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,11 +15,13 @@ public final class EngineStoryItem: Equatable {
|
|||||||
public let seenCount: Int
|
public let seenCount: Int
|
||||||
public let reactedCount: Int
|
public let reactedCount: Int
|
||||||
public let seenPeers: [EnginePeer]
|
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.seenCount = seenCount
|
||||||
self.reactedCount = reactedCount
|
self.reactedCount = reactedCount
|
||||||
self.seenPeers = seenPeers
|
self.seenPeers = seenPeers
|
||||||
|
self.hasList = hasList
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func ==(lhs: Views, rhs: Views) -> Bool {
|
public static func ==(lhs: Views, rhs: Views) -> Bool {
|
||||||
@ -32,6 +34,9 @@ public final class EngineStoryItem: Equatable {
|
|||||||
if lhs.seenPeers != rhs.seenPeers {
|
if lhs.seenPeers != rhs.seenPeers {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.hasList != rhs.hasList {
|
||||||
|
return false
|
||||||
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -154,7 +159,8 @@ extension EngineStoryItem {
|
|||||||
return Stories.Item.Views(
|
return Stories.Item.Views(
|
||||||
seenCount: views.seenCount,
|
seenCount: views.seenCount,
|
||||||
reactedCount: views.reactedCount,
|
reactedCount: views.reactedCount,
|
||||||
seenPeerIds: views.seenPeers.map(\.id)
|
seenPeerIds: views.seenPeers.map(\.id),
|
||||||
|
hasList: views.hasList
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
privacy: self.privacy.flatMap { privacy in
|
privacy: self.privacy.flatMap { privacy in
|
||||||
@ -529,7 +535,8 @@ public final class PeerStoryListContext {
|
|||||||
reactedCount: views.reactedCount,
|
reactedCount: views.reactedCount,
|
||||||
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
||||||
return transaction.getPeer(id).flatMap(EnginePeer.init)
|
return transaction.getPeer(id).flatMap(EnginePeer.init)
|
||||||
}
|
},
|
||||||
|
hasList: views.hasList
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||||
@ -656,7 +663,8 @@ public final class PeerStoryListContext {
|
|||||||
reactedCount: views.reactedCount,
|
reactedCount: views.reactedCount,
|
||||||
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
||||||
return transaction.getPeer(id).flatMap(EnginePeer.init)
|
return transaction.getPeer(id).flatMap(EnginePeer.init)
|
||||||
}
|
},
|
||||||
|
hasList: views.hasList
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||||
@ -807,7 +815,8 @@ public final class PeerStoryListContext {
|
|||||||
reactedCount: views.reactedCount,
|
reactedCount: views.reactedCount,
|
||||||
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
||||||
return peers[id].flatMap(EnginePeer.init)
|
return peers[id].flatMap(EnginePeer.init)
|
||||||
}
|
},
|
||||||
|
hasList: views.hasList
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||||
@ -849,7 +858,8 @@ public final class PeerStoryListContext {
|
|||||||
reactedCount: views.reactedCount,
|
reactedCount: views.reactedCount,
|
||||||
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
||||||
return peers[id].flatMap(EnginePeer.init)
|
return peers[id].flatMap(EnginePeer.init)
|
||||||
}
|
},
|
||||||
|
hasList: views.hasList
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||||
@ -893,7 +903,8 @@ public final class PeerStoryListContext {
|
|||||||
reactedCount: views.reactedCount,
|
reactedCount: views.reactedCount,
|
||||||
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
||||||
return peers[id].flatMap(EnginePeer.init)
|
return peers[id].flatMap(EnginePeer.init)
|
||||||
}
|
},
|
||||||
|
hasList: views.hasList
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||||
@ -933,7 +944,8 @@ public final class PeerStoryListContext {
|
|||||||
reactedCount: views.reactedCount,
|
reactedCount: views.reactedCount,
|
||||||
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
||||||
return peers[id].flatMap(EnginePeer.init)
|
return peers[id].flatMap(EnginePeer.init)
|
||||||
}
|
},
|
||||||
|
hasList: views.hasList
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||||
@ -1097,7 +1109,8 @@ public final class PeerExpiringStoryListContext {
|
|||||||
reactedCount: views.reactedCount,
|
reactedCount: views.reactedCount,
|
||||||
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
||||||
return transaction.getPeer(id).flatMap(EnginePeer.init)
|
return transaction.getPeer(id).flatMap(EnginePeer.init)
|
||||||
}
|
},
|
||||||
|
hasList: views.hasList
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||||
|
@ -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<UploadedPeerPhotoData, NoError> {
|
func _internal_uploadedPeerVideo(postbox: Postbox, network: Network, messageMediaPreuploadManager: MessageMediaPreuploadManager?, resource: MediaResource) -> Signal<UploadedPeerPhotoData, NoError> {
|
||||||
if let messageMediaPreuploadManager = messageMediaPreuploadManager {
|
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
|
|> map { result -> UploadedPeerPhotoData in
|
||||||
return UploadedPeerPhotoData(resource: resource, content: .result(result), local: false)
|
return UploadedPeerPhotoData(resource: resource, content: .result(result), local: false)
|
||||||
}
|
}
|
||||||
|
@ -1171,6 +1171,7 @@ public class CameraScreen: ViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fileprivate var hasGallery = false
|
fileprivate var hasGallery = false
|
||||||
|
fileprivate var postingAvailable = true
|
||||||
|
|
||||||
private var presentationData: PresentationData
|
private var presentationData: PresentationData
|
||||||
private var validLayout: ContainerViewLayout?
|
private var validLayout: ContainerViewLayout?
|
||||||
@ -2154,7 +2155,7 @@ public class CameraScreen: ViewController {
|
|||||||
cameraAuthorizationStatus: self.cameraAuthorizationStatus,
|
cameraAuthorizationStatus: self.cameraAuthorizationStatus,
|
||||||
microphoneAuthorizationStatus: self.microphoneAuthorizationStatus,
|
microphoneAuthorizationStatus: self.microphoneAuthorizationStatus,
|
||||||
hasAppeared: self.hasAppeared,
|
hasAppeared: self.hasAppeared,
|
||||||
isVisible: self.cameraIsActive && !self.hasGallery,
|
isVisible: self.cameraIsActive && !self.hasGallery && self.postingAvailable,
|
||||||
panelWidth: panelWidth,
|
panelWidth: panelWidth,
|
||||||
animateFlipAction: self.animateFlipAction,
|
animateFlipAction: self.animateFlipAction,
|
||||||
animateShutter: { [weak self] in
|
animateShutter: { [weak self] in
|
||||||
@ -2435,6 +2436,8 @@ public class CameraScreen: ViewController {
|
|||||||
guard let self, availability != .available else {
|
guard let self, availability != .available else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
self.node.postingAvailable = false
|
||||||
|
|
||||||
let subject: PremiumLimitSubject
|
let subject: PremiumLimitSubject
|
||||||
switch availability {
|
switch availability {
|
||||||
case .expiringLimit:
|
case .expiringLimit:
|
||||||
@ -2462,7 +2465,9 @@ public class CameraScreen: ViewController {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
let isPremium = peer?.isPremium ?? false
|
let isPremium = peer?.isPremium ?? false
|
||||||
if !isPremium {
|
if isPremium {
|
||||||
|
self.node.postingAvailable = true
|
||||||
|
} else {
|
||||||
self.requestDismiss(animated: true)
|
self.requestDismiss(animated: true)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -2572,6 +2577,8 @@ public class CameraScreen: ViewController {
|
|||||||
self.node.hasGallery = false
|
self.node.hasGallery = false
|
||||||
self.node.requestUpdateLayout(hasAppeared: self.node.hasAppeared, transition: .immediate)
|
self.node.requestUpdateLayout(hasAppeared: self.node.hasAppeared, transition: .immediate)
|
||||||
}
|
}
|
||||||
|
}, groupsPresented: {
|
||||||
|
stopCameraCapture()
|
||||||
})
|
})
|
||||||
self.galleryController = controller
|
self.galleryController = controller
|
||||||
}
|
}
|
||||||
|
@ -2213,7 +2213,7 @@ public protocol EmojiContentPeekBehavior: AnyObject {
|
|||||||
public protocol EmojiCustomContentView: UIView {
|
public protocol EmojiCustomContentView: UIView {
|
||||||
var tintContainerView: UIView { get }
|
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 {
|
public final class EmojiPagerContentComponent: Component {
|
||||||
@ -6562,7 +6562,7 @@ public final class EmojiPagerContentComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let availableCustomContentSize = availableSize
|
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))
|
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
|
customContentHeight = customContentViewSize.height
|
||||||
|
@ -22,7 +22,7 @@ public final class DrawingBubbleEntity: DrawingEntity, Codable {
|
|||||||
case stroke
|
case stroke
|
||||||
}
|
}
|
||||||
|
|
||||||
public let uuid: UUID
|
public var uuid: UUID
|
||||||
public let isAnimated: Bool
|
public let isAnimated: Bool
|
||||||
|
|
||||||
public var drawType: DrawType
|
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)
|
let newEntity = DrawingBubbleEntity(drawType: self.drawType, color: self.color, lineWidth: self.lineWidth)
|
||||||
|
if copy {
|
||||||
|
newEntity.uuid = self.uuid
|
||||||
|
}
|
||||||
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
||||||
newEntity.position = self.position
|
newEntity.position = self.position
|
||||||
newEntity.size = self.size
|
newEntity.size = self.size
|
||||||
|
@ -2,7 +2,7 @@ import Foundation
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
public protocol DrawingEntity: AnyObject {
|
public protocol DrawingEntity: AnyObject {
|
||||||
var uuid: UUID { get }
|
var uuid: UUID { get set }
|
||||||
var isAnimated: Bool { get }
|
var isAnimated: Bool { get }
|
||||||
var center: CGPoint { get }
|
var center: CGPoint { get }
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ public protocol DrawingEntity: AnyObject {
|
|||||||
|
|
||||||
var scale: CGFloat { get set }
|
var scale: CGFloat { get set }
|
||||||
|
|
||||||
func duplicate() -> DrawingEntity
|
func duplicate(copy: Bool) -> DrawingEntity
|
||||||
|
|
||||||
var renderImage: UIImage? { get set }
|
var renderImage: UIImage? { get set }
|
||||||
var renderSubEntities: [DrawingEntity]? { get set }
|
var renderSubEntities: [DrawingEntity]? { get set }
|
||||||
|
@ -11,6 +11,8 @@ public final class DrawingLocationEntity: DrawingEntity, Codable {
|
|||||||
case uuid
|
case uuid
|
||||||
case title
|
case title
|
||||||
case style
|
case style
|
||||||
|
case color
|
||||||
|
case hasCustomColor
|
||||||
case location
|
case location
|
||||||
case icon
|
case icon
|
||||||
case queryId
|
case queryId
|
||||||
@ -27,6 +29,7 @@ public final class DrawingLocationEntity: DrawingEntity, Codable {
|
|||||||
case white
|
case white
|
||||||
case black
|
case black
|
||||||
case transparent
|
case transparent
|
||||||
|
case custom
|
||||||
case blur
|
case blur
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -42,7 +45,18 @@ public final class DrawingLocationEntity: DrawingEntity, Codable {
|
|||||||
public var icon: TelegramMediaFile?
|
public var icon: TelegramMediaFile?
|
||||||
public var queryId: Int64?
|
public var queryId: Int64?
|
||||||
public var resultId: String?
|
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 lineWidth: CGFloat = 0.0
|
||||||
|
|
||||||
public var referenceDrawingSize: CGSize
|
public var referenceDrawingSize: CGSize
|
||||||
@ -88,6 +102,8 @@ public final class DrawingLocationEntity: DrawingEntity, Codable {
|
|||||||
self.uuid = try container.decode(UUID.self, forKey: .uuid)
|
self.uuid = try container.decode(UUID.self, forKey: .uuid)
|
||||||
self.title = try container.decode(String.self, forKey: .title)
|
self.title = try container.decode(String.self, forKey: .title)
|
||||||
self.style = try container.decode(Style.self, forKey: .style)
|
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) {
|
if let locationData = try container.decodeIfPresent(Data.self, forKey: .location) {
|
||||||
self.location = PostboxDecoder(buffer: MemoryBuffer(data: locationData)).decodeRootObject() as! TelegramMediaMap
|
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.uuid, forKey: .uuid)
|
||||||
try container.encode(self.title, forKey: .title)
|
try container.encode(self.title, forKey: .title)
|
||||||
try container.encode(self.style, forKey: .style)
|
try container.encode(self.style, forKey: .style)
|
||||||
|
try container.encode(self.color, forKey: .color)
|
||||||
|
try container.encode(self.hasCustomColor, forKey: .hasCustomColor)
|
||||||
|
|
||||||
var encoder = PostboxEncoder()
|
var encoder = PostboxEncoder()
|
||||||
encoder.encodeRootObject(self.location)
|
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)
|
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.referenceDrawingSize = self.referenceDrawingSize
|
||||||
newEntity.position = self.position
|
newEntity.position = self.position
|
||||||
newEntity.width = self.width
|
newEntity.width = self.width
|
||||||
|
@ -60,7 +60,7 @@ public final class DrawingMediaEntity: DrawingEntity, Codable {
|
|||||||
case mirrored
|
case mirrored
|
||||||
}
|
}
|
||||||
|
|
||||||
public let uuid: UUID
|
public var uuid: UUID
|
||||||
public let content: Content
|
public let content: Content
|
||||||
public let size: CGSize
|
public let size: CGSize
|
||||||
|
|
||||||
@ -157,7 +157,7 @@ public final class DrawingMediaEntity: DrawingEntity, Codable {
|
|||||||
try container.encode(self.mirrored, forKey: .mirrored)
|
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)
|
let newEntity = DrawingMediaEntity(content: self.content, size: self.size)
|
||||||
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
||||||
newEntity.position = self.position
|
newEntity.position = self.position
|
||||||
|
@ -28,7 +28,7 @@ public final class DrawingSimpleShapeEntity: DrawingEntity, Codable {
|
|||||||
case stroke
|
case stroke
|
||||||
}
|
}
|
||||||
|
|
||||||
public let uuid: UUID
|
public var uuid: UUID
|
||||||
public let isAnimated: Bool
|
public let isAnimated: Bool
|
||||||
|
|
||||||
public var shapeType: ShapeType
|
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)
|
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.referenceDrawingSize = self.referenceDrawingSize
|
||||||
newEntity.position = self.position
|
newEntity.position = self.position
|
||||||
newEntity.size = self.size
|
newEntity.size = self.size
|
||||||
|
@ -69,7 +69,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
|||||||
case isExplicitlyStatic
|
case isExplicitlyStatic
|
||||||
}
|
}
|
||||||
|
|
||||||
public let uuid: UUID
|
public var uuid: UUID
|
||||||
public let content: Content
|
public let content: Content
|
||||||
|
|
||||||
public var referenceDrawingSize: CGSize
|
public var referenceDrawingSize: CGSize
|
||||||
@ -221,8 +221,11 @@ public final class DrawingStickerEntity: DrawingEntity, Codable {
|
|||||||
try container.encode(self.isExplicitlyStatic, forKey: .isExplicitlyStatic)
|
try container.encode(self.isExplicitlyStatic, forKey: .isExplicitlyStatic)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func duplicate() -> DrawingEntity {
|
public func duplicate(copy: Bool) -> DrawingEntity {
|
||||||
let newEntity = DrawingStickerEntity(content: self.content)
|
let newEntity = DrawingStickerEntity(content: self.content)
|
||||||
|
if copy {
|
||||||
|
newEntity.uuid = self.uuid
|
||||||
|
}
|
||||||
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
||||||
newEntity.position = self.position
|
newEntity.position = self.position
|
||||||
newEntity.scale = self.scale
|
newEntity.scale = self.scale
|
||||||
|
@ -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)
|
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.referenceDrawingSize = self.referenceDrawingSize
|
||||||
newEntity.position = self.position
|
newEntity.position = self.position
|
||||||
newEntity.width = self.width
|
newEntity.width = self.width
|
||||||
|
@ -23,7 +23,7 @@ public final class DrawingVectorEntity: DrawingEntity, Codable {
|
|||||||
case twoSidedArrow
|
case twoSidedArrow
|
||||||
}
|
}
|
||||||
|
|
||||||
public let uuid: UUID
|
public var uuid: UUID
|
||||||
public let isAnimated: Bool
|
public let isAnimated: Bool
|
||||||
|
|
||||||
public var type: VectorType
|
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)
|
let newEntity = DrawingVectorEntity(type: self.type, color: self.color, lineWidth: self.lineWidth)
|
||||||
|
if copy {
|
||||||
|
newEntity.uuid = self.uuid
|
||||||
|
}
|
||||||
newEntity.drawingSize = self.drawingSize
|
newEntity.drawingSize = self.drawingSize
|
||||||
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
newEntity.referenceDrawingSize = self.referenceDrawingSize
|
||||||
newEntity.start = self.start
|
newEntity.start = self.start
|
||||||
|
@ -503,6 +503,12 @@ public final class MediaEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let player {
|
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 {
|
if let initialSeekPosition = self.initialSeekPosition {
|
||||||
self.initialSeekPosition = nil
|
self.initialSeekPosition = nil
|
||||||
player.seek(to: CMTime(seconds: initialSeekPosition, preferredTimescale: CMTimeScale(1000)), toleranceBefore: .zero, toleranceAfter: .zero)
|
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 {
|
} else if case .forceRendering = mode {
|
||||||
self.forceRendering = true
|
self.forceRendering = true
|
||||||
}
|
}
|
||||||
self.values = f(self.values)
|
let updatedValues = f(self.values)
|
||||||
|
if self.values != updatedValues {
|
||||||
|
self.values = updatedValues
|
||||||
|
}
|
||||||
if case .skipRendering = mode {
|
if case .skipRendering = mode {
|
||||||
self.skipRendering = false
|
self.skipRendering = false
|
||||||
} else if case .forceRendering = mode {
|
} else if case .forceRendering = mode {
|
||||||
|
@ -116,16 +116,24 @@ public final class MediaEditorValues: Codable, Equatable {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if let lhsToolValue = lhsToolValue as? Float, let rhsToolValue = rhsToolValue as? Float {
|
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 {
|
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 {
|
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 {
|
if let lhsToolValue = lhsToolValue as? CurvesValue, let rhsToolValue = rhsToolValue as? CurvesValue {
|
||||||
return lhsToolValue != rhsToolValue
|
if lhsToolValue != rhsToolValue {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1602,7 +1602,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []),
|
privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []),
|
||||||
timeout: 86400,
|
timeout: 86400,
|
||||||
isForwardingDisabled: false,
|
isForwardingDisabled: false,
|
||||||
pin: false
|
pin: true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1857,7 +1857,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
initialValues = draft.values
|
initialValues = draft.values
|
||||||
|
|
||||||
for entity in draft.values.entities {
|
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() {
|
if let drawingData = initialValues?.drawing?.pngData() {
|
||||||
@ -2068,6 +2068,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
self.isInteractingWithEntities = isInteracting
|
self.isInteractingWithEntities = isInteracting
|
||||||
if !isInteracting {
|
if !isInteracting {
|
||||||
self.controller?.isSavingAvailable = true
|
self.controller?.isSavingAvailable = true
|
||||||
|
self.hasAnyChanges = true
|
||||||
}
|
}
|
||||||
self.requestUpdate(transition: .easeInOut(duration: 0.2))
|
self.requestUpdate(transition: .easeInOut(duration: 0.2))
|
||||||
}
|
}
|
||||||
@ -3231,7 +3232,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
if self.entitiesView.selectedEntityView != nil || self.isDisplayingTool {
|
if self.entitiesView.selectedEntityView != nil || self.isDisplayingTool {
|
||||||
bottomInputOffset = inputHeight / 2.0
|
bottomInputOffset = inputHeight / 2.0
|
||||||
} else {
|
} 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(
|
let stateContext = ShareWithPeersScreen.StateContext(
|
||||||
context: self.context,
|
context: self.context,
|
||||||
subject: .stories(editing: false),
|
subject: .stories(editing: false),
|
||||||
|
editing: false,
|
||||||
initialPeerIds: Set(privacy.privacy.additionallyIncludePeers),
|
initialPeerIds: Set(privacy.privacy.additionallyIncludePeers),
|
||||||
closeFriends: self.closeFriends.get(),
|
closeFriends: self.closeFriends.get(),
|
||||||
blockedPeersContext: self.storiesBlockedPeers
|
blockedPeersContext: self.storiesBlockedPeers
|
||||||
@ -3554,11 +3556,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
} else if privacy.base == .nobody {
|
} else if privacy.base == .nobody {
|
||||||
subject = .chats(blocked: false)
|
subject = .chats(blocked: false)
|
||||||
} else {
|
} else {
|
||||||
subject = .contacts(privacy.base)
|
subject = .contacts(base: privacy.base)
|
||||||
}
|
}
|
||||||
let stateContext = ShareWithPeersScreen.StateContext(
|
let stateContext = ShareWithPeersScreen.StateContext(
|
||||||
context: self.context,
|
context: self.context,
|
||||||
subject: subject,
|
subject: subject,
|
||||||
|
editing: false,
|
||||||
initialPeerIds: Set(privacy.additionallyIncludePeers),
|
initialPeerIds: Set(privacy.additionallyIncludePeers),
|
||||||
blockedPeersContext: self.storiesBlockedPeers
|
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
|
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 {
|
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)
|
self.push(controller)
|
||||||
}
|
}
|
||||||
return false }
|
return false }
|
||||||
@ -3971,6 +3974,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
var caption = self.getCaption()
|
var caption = self.getCaption()
|
||||||
caption = convertMarkdownToAttributes(caption)
|
caption = convertMarkdownToAttributes(caption)
|
||||||
|
|
||||||
|
var hasEntityChanges = false
|
||||||
let randomId: Int64
|
let randomId: Int64
|
||||||
if case let .draft(_, id) = subject, let id {
|
if case let .draft(_, id) = subject, let id {
|
||||||
randomId = id
|
randomId = id
|
||||||
@ -3979,7 +3983,10 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
|
|||||||
}
|
}
|
||||||
|
|
||||||
var mediaAreas: [MediaArea] = []
|
var mediaAreas: [MediaArea] = []
|
||||||
if case .draft = subject {
|
if case let .draft(draft, _) = subject {
|
||||||
|
if draft.values.entities != codableEntities {
|
||||||
|
hasEntityChanges = true
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
mediaAreas = self.initialMediaAreas ?? []
|
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.completion(randomId, nil, [], caption, self.state.privacy, stickers, { [weak self] finished in
|
||||||
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
||||||
self?.dismiss()
|
self?.dismiss()
|
||||||
|
@ -1119,8 +1119,8 @@ public final class MessageInputPanelComponent: Component {
|
|||||||
theme: component.theme,
|
theme: component.theme,
|
||||||
strings: component.strings,
|
strings: component.strings,
|
||||||
presentController: component.presentController,
|
presentController: component.presentController,
|
||||||
audioRecorder: component.audioRecorder,
|
audioRecorder: nil,
|
||||||
videoRecordingStatus: component.videoRecordingStatus
|
videoRecordingStatus: nil
|
||||||
)),
|
)),
|
||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: 33.0, height: 33.0)
|
containerSize: CGSize(width: 33.0, height: 33.0)
|
||||||
|
@ -26,6 +26,7 @@ public final class NavigationSearchComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public let colors: Colors
|
public let colors: Colors
|
||||||
|
public let cancel: String
|
||||||
public let placeholder: String
|
public let placeholder: String
|
||||||
public let isSearchActive: Bool
|
public let isSearchActive: Bool
|
||||||
public let collapseFraction: CGFloat
|
public let collapseFraction: CGFloat
|
||||||
@ -35,6 +36,7 @@ public final class NavigationSearchComponent: Component {
|
|||||||
|
|
||||||
public init(
|
public init(
|
||||||
colors: Colors,
|
colors: Colors,
|
||||||
|
cancel: String,
|
||||||
placeholder: String,
|
placeholder: String,
|
||||||
isSearchActive: Bool,
|
isSearchActive: Bool,
|
||||||
collapseFraction: CGFloat,
|
collapseFraction: CGFloat,
|
||||||
@ -43,6 +45,7 @@ public final class NavigationSearchComponent: Component {
|
|||||||
updateQuery: @escaping (String) -> Void
|
updateQuery: @escaping (String) -> Void
|
||||||
) {
|
) {
|
||||||
self.colors = colors
|
self.colors = colors
|
||||||
|
self.cancel = cancel
|
||||||
self.placeholder = placeholder
|
self.placeholder = placeholder
|
||||||
self.isSearchActive = isSearchActive
|
self.isSearchActive = isSearchActive
|
||||||
self.collapseFraction = collapseFraction
|
self.collapseFraction = collapseFraction
|
||||||
@ -55,6 +58,9 @@ public final class NavigationSearchComponent: Component {
|
|||||||
if lhs.colors != rhs.colors {
|
if lhs.colors != rhs.colors {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.cancel != rhs.cancel {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.placeholder != rhs.placeholder {
|
if lhs.placeholder != rhs.placeholder {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -189,11 +195,10 @@ public final class NavigationSearchComponent: Component {
|
|||||||
self.button = button
|
self.button = button
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO:localize
|
|
||||||
let buttonSize = button.update(
|
let buttonSize = button.update(
|
||||||
transition: buttonTransition,
|
transition: buttonTransition,
|
||||||
component: AnyComponent(Button(
|
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
|
action: { [weak self] in
|
||||||
guard let self, let component = self.component else {
|
guard let self, let component = self.component else {
|
||||||
return
|
return
|
||||||
|
@ -1626,7 +1626,7 @@ final class ShareWithPeersScreenComponent: Component {
|
|||||||
if let searchStateContext = self.searchStateContext, searchStateContext.subject == .search(query: self.navigationTextFieldState.text, onlyContacts: onlyContacts) {
|
if let searchStateContext = self.searchStateContext, searchStateContext.subject == .search(query: self.navigationTextFieldState.text, onlyContacts: onlyContacts) {
|
||||||
} else {
|
} else {
|
||||||
self.searchStateDisposable?.dispose()
|
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
|
var applyState = false
|
||||||
self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
|
self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
@ -1667,6 +1667,10 @@ final class ShareWithPeersScreenComponent: Component {
|
|||||||
environment: {},
|
environment: {},
|
||||||
containerSize: CGSize(width: itemsContainerWidth, height: 1000.0)
|
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(
|
let peerItemSize = self.peerTemplateItem.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(PeerListItemComponent(
|
component: AnyComponent(PeerListItemComponent(
|
||||||
@ -1677,7 +1681,7 @@ final class ShareWithPeersScreenComponent: Component {
|
|||||||
sideInset: sideInset,
|
sideInset: sideInset,
|
||||||
title: "Name",
|
title: "Name",
|
||||||
peer: nil,
|
peer: nil,
|
||||||
subtitle: self.searchStateContext != nil ? "" : "sub",
|
subtitle: isContactsSearch ? "" : "sub",
|
||||||
subtitleAccessory: .none,
|
subtitleAccessory: .none,
|
||||||
presence: nil,
|
presence: nil,
|
||||||
selectionState: .editing(isSelected: false, isTinted: false),
|
selectionState: .editing(isSelected: false, isTinted: false),
|
||||||
@ -1977,7 +1981,9 @@ final class ShareWithPeersScreenComponent: Component {
|
|||||||
|
|
||||||
let proceed = {
|
let proceed = {
|
||||||
var savePeers = true
|
var savePeers = true
|
||||||
if base == .closeFriends {
|
if component.stateContext.editing {
|
||||||
|
savePeers = false
|
||||||
|
} else if base == .closeFriends {
|
||||||
savePeers = false
|
savePeers = false
|
||||||
} else {
|
} else {
|
||||||
if case .stories = component.stateContext.subject {
|
if case .stories = component.stateContext.subject {
|
||||||
@ -2023,7 +2029,7 @@ final class ShareWithPeersScreenComponent: Component {
|
|||||||
|
|
||||||
}
|
}
|
||||||
if savePeers {
|
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: {
|
|> deliverOnMainQueue).start(completed: {
|
||||||
complete()
|
complete()
|
||||||
})
|
})
|
||||||
@ -2261,13 +2267,14 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
public enum Subject: Equatable {
|
public enum Subject: Equatable {
|
||||||
case stories(editing: Bool)
|
case stories(editing: Bool)
|
||||||
case chats(blocked: Bool)
|
case chats(blocked: Bool)
|
||||||
case contacts(EngineStoryPrivacy.Base)
|
case contacts(base: EngineStoryPrivacy.Base)
|
||||||
case search(query: String, onlyContacts: Bool)
|
case search(query: String, onlyContacts: Bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
fileprivate var stateValue: State?
|
fileprivate var stateValue: State?
|
||||||
|
|
||||||
public let subject: Subject
|
public let subject: Subject
|
||||||
|
public let editing: Bool
|
||||||
public private(set) var initialPeerIds: Set<EnginePeer.Id> = Set()
|
public private(set) var initialPeerIds: Set<EnginePeer.Id> = Set()
|
||||||
fileprivate let blockedPeersContext: BlockedPeersContext?
|
fileprivate let blockedPeersContext: BlockedPeersContext?
|
||||||
|
|
||||||
@ -2284,11 +2291,14 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
public init(
|
public init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
subject: Subject = .chats(blocked: false),
|
subject: Subject = .chats(blocked: false),
|
||||||
|
editing: Bool,
|
||||||
|
initialSelectedPeers: [EngineStoryPrivacy.Base: [EnginePeer.Id]] = [:],
|
||||||
initialPeerIds: Set<EnginePeer.Id> = Set(),
|
initialPeerIds: Set<EnginePeer.Id> = Set(),
|
||||||
closeFriends: Signal<[EnginePeer], NoError> = .single([]),
|
closeFriends: Signal<[EnginePeer], NoError> = .single([]),
|
||||||
blockedPeersContext: BlockedPeersContext? = nil
|
blockedPeersContext: BlockedPeersContext? = nil
|
||||||
) {
|
) {
|
||||||
self.subject = subject
|
self.subject = subject
|
||||||
|
self.editing = editing
|
||||||
self.initialPeerIds = initialPeerIds
|
self.initialPeerIds = initialPeerIds
|
||||||
self.blockedPeersContext = blockedPeersContext
|
self.blockedPeersContext = blockedPeersContext
|
||||||
|
|
||||||
@ -2313,20 +2323,34 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
savedContactsExceptionPeers,
|
savedContactsExceptionPeers,
|
||||||
savedSelectedPeers
|
savedSelectedPeers
|
||||||
) |> mapToSignal { everyone, contacts, selected -> Signal<([EnginePeer.Id: EnginePeer], [EnginePeer.Id], [EnginePeer.Id], [EnginePeer.Id]), NoError> in
|
) |> 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<EnginePeer?, NoError>] = []
|
var everyonePeerSignals: [Signal<EnginePeer?, NoError>] = []
|
||||||
if everyone.count < 3 {
|
if everyone.count < 3 {
|
||||||
for peerId in everyone {
|
for peerId in everyone {
|
||||||
everyonePeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)))
|
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<EnginePeer?, NoError>] = []
|
var contactsPeerSignals: [Signal<EnginePeer?, NoError>] = []
|
||||||
if contacts.count < 3 {
|
if contacts.count < 3 {
|
||||||
for peerId in contacts {
|
for peerId in contacts {
|
||||||
contactsPeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)))
|
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<EnginePeer?, NoError>] = []
|
var selectedPeerSignals: [Signal<EnginePeer?, NoError>] = []
|
||||||
if contacts.count < 3 {
|
if selected.count < 3 {
|
||||||
for peerId in selected {
|
for peerId in selected {
|
||||||
selectedPeerSignals.append(context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)))
|
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)
|
self.readySubject.set(true)
|
||||||
})
|
})
|
||||||
case let .search(query, onlyContacts):
|
case let .search(query, onlyContacts):
|
||||||
let signal: Signal<([EngineRenderedPeer], [EnginePeer.Id: Optional<Int>]), NoError>
|
let signal: Signal<([EngineRenderedPeer], [EnginePeer.Id: Optional<EnginePeer.Presence>], [EnginePeer.Id: Optional<Int>]), NoError>
|
||||||
if onlyContacts {
|
if onlyContacts {
|
||||||
signal = combineLatest(
|
signal = combineLatest(
|
||||||
context.engine.contacts.searchLocalPeers(query: query),
|
context.engine.contacts.searchLocalPeers(query: query),
|
||||||
@ -2584,25 +2608,33 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
)
|
)
|
||||||
|> map { peers, contacts in
|
|> map { peers, contacts in
|
||||||
let contactIds = Set(contacts.0.map { $0.id })
|
let contactIds = Set(contacts.0.map { $0.id })
|
||||||
return (peers.filter { contactIds.contains($0.peerId) }, [:])
|
return (peers.filter { contactIds.contains($0.peerId) }, [:], [:])
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
signal = context.engine.contacts.searchLocalPeers(query: query)
|
signal = context.engine.contacts.searchLocalPeers(query: query)
|
||||||
|> mapToSignal { peers in
|
|> mapToSignal { peers in
|
||||||
return context.engine.data.subscribe(
|
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))
|
EngineDataMap(peers.map(\.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init))
|
||||||
)
|
)
|
||||||
|> map { participantCountMap -> ([EngineRenderedPeer], [EnginePeer.Id: Optional<Int>]) in
|
|> map { presenceMap, participantCountMap -> ([EngineRenderedPeer], [EnginePeer.Id: Optional<EnginePeer.Presence>], [EnginePeer.Id: Optional<Int>]) in
|
||||||
return (peers, participantCountMap)
|
return (peers, presenceMap, participantCountMap)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.stateDisposable = (signal
|
self.stateDisposable = (signal
|
||||||
|> deliverOnMainQueue).start(next: { [weak self] peers, participantCounts in
|
|> deliverOnMainQueue).start(next: { [weak self] peers, presenceMap, participantCounts in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var presences: [EnginePeer.Id: EnginePeer.Presence] = [:]
|
||||||
|
for (key, value) in presenceMap {
|
||||||
|
if let value {
|
||||||
|
presences[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var participants: [EnginePeer.Id: Int] = [:]
|
var participants: [EnginePeer.Id: Int] = [:]
|
||||||
for (key, value) in participantCounts {
|
for (key, value) in participantCounts {
|
||||||
if let value {
|
if let value {
|
||||||
@ -2638,7 +2670,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
|||||||
},
|
},
|
||||||
peersMap: [:],
|
peersMap: [:],
|
||||||
savedSelectedPeers: [:],
|
savedSelectedPeers: [:],
|
||||||
presences: [:],
|
presences: presences,
|
||||||
participants: participants,
|
participants: participants,
|
||||||
closeFriendsPeers: [],
|
closeFriendsPeers: [],
|
||||||
grayListPeers: []
|
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<Never, NoError> {
|
private func updatePeersListStoredState(engine: TelegramEngine, base: Stories.Item.Privacy.Base, peerIds: [EnginePeer.Id]) -> Signal<Never, NoError> {
|
||||||
let key = EngineDataBuffer(length: 4)
|
let key = EngineDataBuffer(length: 4)
|
||||||
key.setInt32(0, value: base.rawValue)
|
key.setInt32(0, value: base.rawValue)
|
||||||
|
|
||||||
|
@ -161,7 +161,8 @@ public final class StoryContentContextImpl: StoryContentContext {
|
|||||||
reactedCount: views.reactedCount,
|
reactedCount: views.reactedCount,
|
||||||
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
||||||
return peers[id].flatMap(EnginePeer.init)
|
return peers[id].flatMap(EnginePeer.init)
|
||||||
}
|
},
|
||||||
|
hasList: views.hasList
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
privacy: item.privacy.flatMap(EngineStoryPrivacy.init),
|
||||||
@ -1037,7 +1038,8 @@ public final class SingleStoryContentContextImpl: StoryContentContext {
|
|||||||
reactedCount: views.reactedCount,
|
reactedCount: views.reactedCount,
|
||||||
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
seenPeers: views.seenPeerIds.compactMap { id -> EnginePeer? in
|
||||||
return peers[id].flatMap(EnginePeer.init)
|
return peers[id].flatMap(EnginePeer.init)
|
||||||
}
|
},
|
||||||
|
hasList: views.hasList
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
privacy: itemValue.privacy.flatMap(EngineStoryPrivacy.init),
|
privacy: itemValue.privacy.flatMap(EngineStoryPrivacy.init),
|
||||||
|
@ -719,7 +719,7 @@ final class StoryItemContentComponent: Component {
|
|||||||
if let current = self.loadingEffectView {
|
if let current = self.loadingEffectView {
|
||||||
loadingEffectView = current
|
loadingEffectView = current
|
||||||
} else {
|
} 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.loadingEffectView = loadingEffectView
|
||||||
self.addSubview(loadingEffectView)
|
self.addSubview(loadingEffectView)
|
||||||
}
|
}
|
||||||
@ -743,21 +743,21 @@ final class StoryItemContentComponent: Component {
|
|||||||
if let current = self.mediaAreasEffectView {
|
if let current = self.mediaAreasEffectView {
|
||||||
mediaAreasEffectView = current
|
mediaAreasEffectView = current
|
||||||
} else {
|
} 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.mediaAreasEffectView = mediaAreasEffectView
|
||||||
self.addSubview(mediaAreasEffectView)
|
self.addSubview(mediaAreasEffectView)
|
||||||
}
|
}
|
||||||
mediaAreasEffectView.update(size: availableSize, transition: transition)
|
mediaAreasEffectView.update(size: availableSize, transition: transition)
|
||||||
|
|
||||||
let maskView: UIView
|
let maskLayer: CALayer
|
||||||
if let current = mediaAreasEffectView.mask {
|
if let current = mediaAreasEffectView.layer.mask {
|
||||||
maskView = current
|
maskLayer = current
|
||||||
} else {
|
} else {
|
||||||
maskView = UIView(frame: CGRect(origin: .zero, size: availableSize))
|
maskLayer = CALayer()
|
||||||
mediaAreasEffectView.mask = maskView
|
mediaAreasEffectView.layer.mask = maskLayer
|
||||||
}
|
}
|
||||||
|
|
||||||
if maskView.subviews.isEmpty {
|
if (maskLayer.sublayers ?? []).isEmpty {
|
||||||
let referenceSize = availableSize
|
let referenceSize = availableSize
|
||||||
for mediaArea in component.item.mediaAreas {
|
for mediaArea in component.item.mediaAreas {
|
||||||
guard case .venue = mediaArea else {
|
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 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 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()
|
let layer = CALayer()
|
||||||
view.backgroundColor = .white
|
layer.backgroundColor = UIColor.white.cgColor
|
||||||
view.bounds = CGRect(origin: .zero, size: size)
|
layer.bounds = CGRect(origin: .zero, size: size)
|
||||||
view.center = position
|
layer.position = position
|
||||||
view.layer.cornerRadius = size.height * 0.18
|
layer.cornerRadius = cornerRadius
|
||||||
maskView.addSubview(view)
|
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 {
|
} else if let mediaAreasEffectView = self.mediaAreasEffectView {
|
||||||
|
@ -5,8 +5,8 @@ import ComponentFlow
|
|||||||
import Display
|
import Display
|
||||||
|
|
||||||
final class StoryItemLoadingEffectView: UIView {
|
final class StoryItemLoadingEffectView: UIView {
|
||||||
private let effectAlpha: CGFloat
|
|
||||||
private let duration: Double
|
private let duration: Double
|
||||||
|
private let hasCustomBorder: Bool
|
||||||
private let playOnce: Bool
|
private let playOnce: Bool
|
||||||
|
|
||||||
private let hierarchyTrackingLayer: HierarchyTrackingLayer
|
private let hierarchyTrackingLayer: HierarchyTrackingLayer
|
||||||
@ -16,18 +16,18 @@ final class StoryItemLoadingEffectView: UIView {
|
|||||||
|
|
||||||
private let borderGradientView: UIImageView
|
private let borderGradientView: UIImageView
|
||||||
private let borderContainerView: UIView
|
private let borderContainerView: UIView
|
||||||
private let borderMaskLayer: SimpleShapeLayer
|
let borderMaskLayer: SimpleShapeLayer
|
||||||
|
|
||||||
private var didPlayOnce = false
|
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.hierarchyTrackingLayer = HierarchyTrackingLayer()
|
||||||
|
|
||||||
self.effectAlpha = effectAlpha
|
|
||||||
self.duration = duration
|
self.duration = duration
|
||||||
|
self.hasCustomBorder = hasCustomBorder
|
||||||
self.playOnce = playOnce
|
self.playOnce = playOnce
|
||||||
|
|
||||||
self.gradientWidth = 200.0
|
self.gradientWidth = gradientWidth
|
||||||
self.backgroundView = UIImageView()
|
self.backgroundView = UIImageView()
|
||||||
|
|
||||||
self.borderGradientView = UIImageView()
|
self.borderGradientView = UIImageView()
|
||||||
@ -75,16 +75,14 @@ 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())
|
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)
|
self.addSubview(self.backgroundView)
|
||||||
|
|
||||||
if hasBorder {
|
self.borderGradientView.image = generateGradient(borderAlpha)
|
||||||
self.borderGradientView.image = generateGradient(self.effectAlpha + 0.1)
|
|
||||||
self.borderContainerView.addSubview(self.borderGradientView)
|
self.borderContainerView.addSubview(self.borderGradientView)
|
||||||
self.addSubview(self.borderContainerView)
|
self.addSubview(self.borderContainerView)
|
||||||
self.borderContainerView.layer.mask = self.borderMaskLayer
|
self.borderContainerView.layer.mask = self.borderMaskLayer
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
@ -107,12 +105,14 @@ final class StoryItemLoadingEffectView: UIView {
|
|||||||
if self.backgroundView.bounds.size != size {
|
if self.backgroundView.bounds.size != size {
|
||||||
self.backgroundView.layer.removeAllAnimations()
|
self.backgroundView.layer.removeAllAnimations()
|
||||||
|
|
||||||
|
if !self.hasCustomBorder {
|
||||||
self.borderMaskLayer.fillColor = nil
|
self.borderMaskLayer.fillColor = nil
|
||||||
self.borderMaskLayer.strokeColor = UIColor.white.cgColor
|
self.borderMaskLayer.strokeColor = UIColor.white.cgColor
|
||||||
let lineWidth: CGFloat = 3.0
|
let lineWidth: CGFloat = 3.0
|
||||||
self.borderMaskLayer.lineWidth = lineWidth
|
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
|
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)))
|
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: -self.gradientWidth, y: 0.0), size: CGSize(width: self.gradientWidth, height: size.height)))
|
||||||
|
|
||||||
|
@ -390,6 +390,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
private let scroller: Scroller
|
private let scroller: Scroller
|
||||||
|
|
||||||
let componentContainerView: UIView
|
let componentContainerView: UIView
|
||||||
|
let overlayContainerView: SparseContainerView
|
||||||
let itemsContainerView: UIView
|
let itemsContainerView: UIView
|
||||||
let controlsContainerView: UIView
|
let controlsContainerView: UIView
|
||||||
let controlsClippingView: UIView
|
let controlsClippingView: UIView
|
||||||
@ -440,6 +441,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
weak var disappearingReactionContextNode: ReactionContextNode?
|
weak var disappearingReactionContextNode: ReactionContextNode?
|
||||||
var displayLikeReactions: Bool = false
|
var displayLikeReactions: Bool = false
|
||||||
var waitingForReactionAnimateOutToLike: MessageReaction.Reaction?
|
var waitingForReactionAnimateOutToLike: MessageReaction.Reaction?
|
||||||
|
private weak var standaloneReactionAnimation: StandaloneReactionAnimation?
|
||||||
|
|
||||||
weak var contextController: ContextController?
|
weak var contextController: ContextController?
|
||||||
weak var privacyController: ShareWithPeersScreen?
|
weak var privacyController: ShareWithPeersScreen?
|
||||||
@ -473,6 +475,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
self.sendMessageContext = StoryItemSetContainerSendMessage()
|
self.sendMessageContext = StoryItemSetContainerSendMessage()
|
||||||
|
|
||||||
self.componentContainerView = UIView()
|
self.componentContainerView = UIView()
|
||||||
|
self.overlayContainerView = SparseContainerView()
|
||||||
|
|
||||||
self.itemsContainerView = UIView()
|
self.itemsContainerView = UIView()
|
||||||
|
|
||||||
@ -514,6 +517,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
self.addSubview(self.componentContainerView)
|
self.addSubview(self.componentContainerView)
|
||||||
|
self.addSubview(self.overlayContainerView)
|
||||||
|
|
||||||
self.itemsContainerView.addSubview(self.scroller)
|
self.itemsContainerView.addSubview(self.scroller)
|
||||||
self.scroller.delegate = self
|
self.scroller.delegate = self
|
||||||
@ -530,7 +534,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
self.componentContainerView.addSubview(self.viewListsContainer)
|
self.componentContainerView.addSubview(self.viewListsContainer)
|
||||||
|
|
||||||
self.closeButton.addSubview(self.closeButtonIconView)
|
self.closeButton.addSubview(self.closeButtonIconView)
|
||||||
self.addSubview(self.closeButton)
|
self.overlayContainerView.addSubview(self.closeButton)
|
||||||
self.closeButton.addTarget(self, action: #selector(self.closePressed), for: .touchUpInside)
|
self.closeButton.addTarget(self, action: #selector(self.closePressed), for: .touchUpInside)
|
||||||
|
|
||||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
|
||||||
@ -548,11 +552,21 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.reactionContextNode != nil {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
if hasFirstResponder(self) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
if self.itemsContainerView.frame.contains(point) {
|
if self.itemsContainerView.frame.contains(point) {
|
||||||
|
if self.viewListDisplayState != .hidden {
|
||||||
|
} else {
|
||||||
if !self.isPointInsideContentArea(point: point) {
|
if !self.isPointInsideContentArea(point: point) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return [.down]
|
return [.down]
|
||||||
})
|
})
|
||||||
@ -840,6 +854,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
|
|
||||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||||
if case .ended = recognizer.state, let component = self.component, let itemLayout = self.itemLayout {
|
if case .ended = recognizer.state, let component = self.component, let itemLayout = self.itemLayout {
|
||||||
|
|
||||||
if let _ = self.sendMessageContext.menuController {
|
if let _ = self.sendMessageContext.menuController {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -885,6 +900,8 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
|
|
||||||
var selectedMediaArea: MediaArea?
|
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 {
|
func isPoint(_ point: CGPoint, in area: MediaArea) -> Bool {
|
||||||
let tx = point.x - area.coordinates.x / 100.0 * referenceSize.width
|
let tx = point.x - area.coordinates.x / 100.0 * referenceSize.width
|
||||||
let ty = point.y - area.coordinates.y / 100.0 * referenceSize.height
|
let ty = point.y - area.coordinates.y / 100.0 * referenceSize.height
|
||||||
@ -904,6 +921,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let selectedMediaArea {
|
if let selectedMediaArea {
|
||||||
self.sendMessageContext.activateMediaArea(view: self, mediaArea: selectedMediaArea)
|
self.sendMessageContext.activateMediaArea(view: self, mediaArea: selectedMediaArea)
|
||||||
@ -1636,6 +1654,10 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
footerAlpha = 0.0
|
footerAlpha = 0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if case .regular = component.metrics.widthClass {
|
||||||
|
footerAlpha *= itemAlpha
|
||||||
|
}
|
||||||
|
|
||||||
itemTransition.setAlpha(view: footerPanelView, alpha: footerAlpha)
|
itemTransition.setAlpha(view: footerPanelView, alpha: footerAlpha)
|
||||||
}
|
}
|
||||||
} else if let footerPanel = visibleItem.footerPanel {
|
} else if let footerPanel = visibleItem.footerPanel {
|
||||||
@ -2099,6 +2121,12 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
removeOnCompletion: false
|
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 {
|
if !transitionOut.destinationIsAvatar {
|
||||||
let transitionView = transitionOut.transitionView
|
let transitionView = transitionOut.transitionView
|
||||||
|
|
||||||
@ -2216,11 +2244,10 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO:localize
|
|
||||||
let tooltipScreen = TooltipScreen(
|
let tooltipScreen = TooltipScreen(
|
||||||
account: component.context.account,
|
account: component.context.account,
|
||||||
sharedContext: component.context.sharedContext,
|
sharedContext: component.context.sharedContext,
|
||||||
text: .markdown(text: "Long tap for more reactions"),
|
text: .markdown(text: component.strings.Story_LongTapForMoreReactions),
|
||||||
balancedTextLayout: true,
|
balancedTextLayout: true,
|
||||||
style: .customBlur(component.theme.rootController.navigationBar.blurredBackgroundColor, 0.0),
|
style: .customBlur(component.theme.rootController.navigationBar.blurredBackgroundColor, 0.0),
|
||||||
arrowStyle: .small,
|
arrowStyle: .small,
|
||||||
@ -2268,6 +2295,24 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
tooltipScreen.dismiss()
|
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 itemsTransition = transition
|
||||||
var resetScrollingOffsetWithItemTransition = false
|
var resetScrollingOffsetWithItemTransition = false
|
||||||
@ -2349,6 +2394,10 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
transition.setBounds(view: self.componentContainerView, bounds: CGRect(origin: CGPoint(), size: availableSize))
|
transition.setBounds(view: self.componentContainerView, bounds: CGRect(origin: CGPoint(), size: availableSize))
|
||||||
transition.setScale(view: self.componentContainerView, scale: dismissPanScale)
|
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
|
var bottomContentInset: CGFloat
|
||||||
if !component.safeInsets.bottom.isZero {
|
if !component.safeInsets.bottom.isZero {
|
||||||
bottomContentInset = component.safeInsets.bottom + 1.0
|
bottomContentInset = component.safeInsets.bottom + 1.0
|
||||||
@ -2380,8 +2429,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
|
|
||||||
let inputPlaceholder: String
|
let inputPlaceholder: String
|
||||||
if let stealthModeTimeout = component.stealthModeTimeout {
|
if let stealthModeTimeout = component.stealthModeTimeout {
|
||||||
//TODO:localize
|
inputPlaceholder = component.strings.Story_StealthModeActivePlaceholder("\(stringForDuration(stealthModeTimeout))").string
|
||||||
inputPlaceholder = "Stealth Mode active – \(stringForDuration(stealthModeTimeout))"
|
|
||||||
} else {
|
} else {
|
||||||
inputPlaceholder = component.strings.Story_InputPlaceholderReplyPrivately
|
inputPlaceholder = component.strings.Story_InputPlaceholderReplyPrivately
|
||||||
}
|
}
|
||||||
@ -2915,7 +2963,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isBlockedFromStories {
|
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)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Stories"), color: theme.contextMenu.primaryColor)
|
||||||
}, action: { [weak self] _, f in
|
}, action: { [weak self] _, f in
|
||||||
f(.default)
|
f(.default)
|
||||||
@ -2929,7 +2977,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||||
self.component?.presentController(UndoOverlayController(
|
self.component?.presentController(UndoOverlayController(
|
||||||
presentationData: presentationData,
|
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,
|
elevatedLayout: false,
|
||||||
position: .top,
|
position: .top,
|
||||||
animateInAsReplacement: false,
|
animateInAsReplacement: false,
|
||||||
@ -2938,7 +2986,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
), nil)
|
), nil)
|
||||||
})))
|
})))
|
||||||
} else {
|
} 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)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Stories"), color: theme.contextMenu.primaryColor)
|
||||||
}, action: { [weak self] _, f in
|
}, action: { [weak self] _, f in
|
||||||
f(.default)
|
f(.default)
|
||||||
@ -2950,7 +2998,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||||
self.component?.presentController(UndoOverlayController(
|
self.component?.presentController(UndoOverlayController(
|
||||||
presentationData: presentationData,
|
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,
|
elevatedLayout: false,
|
||||||
position: .top,
|
position: .top,
|
||||||
animateInAsReplacement: false,
|
animateInAsReplacement: false,
|
||||||
@ -2961,8 +3009,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if isContact {
|
if isContact {
|
||||||
//TODO:localize
|
itemList.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextDeleteContact, textColor: .destructive, icon: { theme in
|
||||||
itemList.append(.action(ContextMenuActionItem(text: "Delete Contact", textColor: .destructive, icon: { theme in
|
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor)
|
||||||
}, action: { [weak self] _, f in
|
}, action: { [weak self] _, f in
|
||||||
f(.default)
|
f(.default)
|
||||||
@ -2984,8 +3031,8 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
"info2.info2.Fill": animationBackgroundColor
|
"info2.info2.Fill": animationBackgroundColor
|
||||||
],
|
],
|
||||||
title: nil,
|
title: nil,
|
||||||
text: "**\(peer.compactDisplayTitle)** has been removed from your contacts.",
|
text: component.strings.Story_ToastDeletedContact(peer.compactDisplayTitle).string,
|
||||||
customUndoText: "Undo",
|
customUndoText: component.strings.Undo_Undo,
|
||||||
timeout: nil
|
timeout: nil
|
||||||
),
|
),
|
||||||
elevatedLayout: false,
|
elevatedLayout: false,
|
||||||
@ -3031,8 +3078,8 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
"info2.info2.Fill": animationBackgroundColor
|
"info2.info2.Fill": animationBackgroundColor
|
||||||
],
|
],
|
||||||
title: nil,
|
title: nil,
|
||||||
text: "**\(peer.compactDisplayTitle)** has been blocked.",
|
text: component.strings.Story_ToastUserBlocked(peer.compactDisplayTitle).string,
|
||||||
customUndoText: "Undo",
|
customUndoText: component.strings.Undo_Undo,
|
||||||
timeout: nil
|
timeout: nil
|
||||||
),
|
),
|
||||||
elevatedLayout: false,
|
elevatedLayout: false,
|
||||||
@ -3257,6 +3304,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
closeButtonFrame.origin.y -= contentBottomInsetOverflow
|
closeButtonFrame.origin.y -= contentBottomInsetOverflow
|
||||||
|
|
||||||
transition.setFrame(view: self.closeButton, frame: closeButtonFrame)
|
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))
|
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
|
headerRightOffset -= 51.0
|
||||||
}
|
}
|
||||||
@ -3908,6 +3956,13 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let standaloneReactionAnimation = self.standaloneReactionAnimation {
|
||||||
|
self.standaloneReactionAnimation = nil
|
||||||
|
standaloneReactionAnimation.view.removeFromSuperview()
|
||||||
|
}
|
||||||
|
self.standaloneReactionAnimation = standaloneReactionAnimation
|
||||||
|
|
||||||
standaloneReactionAnimation.frame = self.bounds
|
standaloneReactionAnimation.frame = self.bounds
|
||||||
self.addSubview(standaloneReactionAnimation.view)
|
self.addSubview(standaloneReactionAnimation.view)
|
||||||
}, completion: { [weak targetView, weak reactionContextNode] in
|
}, completion: { [weak targetView, weak reactionContextNode] in
|
||||||
@ -4085,6 +4140,13 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let standaloneReactionAnimation = self.standaloneReactionAnimation {
|
||||||
|
self.standaloneReactionAnimation = nil
|
||||||
|
standaloneReactionAnimation.view.removeFromSuperview()
|
||||||
|
}
|
||||||
|
self.standaloneReactionAnimation = standaloneReactionAnimation
|
||||||
|
|
||||||
standaloneReactionAnimation.frame = self.bounds
|
standaloneReactionAnimation.frame = self.bounds
|
||||||
self.componentContainerView.addSubview(standaloneReactionAnimation.view)
|
self.componentContainerView.addSubview(standaloneReactionAnimation.view)
|
||||||
}, completion: { [weak reactionContextNode] in
|
}, completion: { [weak reactionContextNode] in
|
||||||
@ -4271,22 +4333,34 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
self.component?.presentController(controller, nil)
|
self.component?.presentController(controller, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func openItemPrivacySettings(initialPrivacy: EngineStoryPrivacy? = nil) {
|
private func openItemPrivacySettings(updatedPrivacy: EngineStoryPrivacy? = nil) {
|
||||||
guard let component = self.component else {
|
guard let component = self.component else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let context = component.context
|
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 {
|
guard let privacy else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var selectedPeers: [EngineStoryPrivacy.Base: [EnginePeer.Id]] = [:]
|
||||||
|
selectedPeers[currentPrivacy.base] = currentPrivacy.additionallyIncludePeers
|
||||||
|
if let updatedPrivacy {
|
||||||
|
selectedPeers[updatedPrivacy.base] = updatedPrivacy.additionallyIncludePeers
|
||||||
|
}
|
||||||
|
|
||||||
let stateContext = ShareWithPeersScreen.StateContext(
|
let stateContext = ShareWithPeersScreen.StateContext(
|
||||||
context: context,
|
context: context,
|
||||||
subject: .stories(editing: true),
|
subject: .stories(editing: true),
|
||||||
initialPeerIds: Set(privacy.additionallyIncludePeers),
|
editing: true,
|
||||||
|
initialSelectedPeers: selectedPeers,
|
||||||
closeFriends: component.closeFriends.get(),
|
closeFriends: component.closeFriends.get(),
|
||||||
blockedPeersContext: component.blockedPeers
|
blockedPeersContext: component.blockedPeers
|
||||||
)
|
)
|
||||||
@ -4303,6 +4377,8 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if component.slice.item.storyItem.privacy == privacy {
|
if component.slice.item.storyItem.privacy == privacy {
|
||||||
|
self.privacyController = nil
|
||||||
|
self.updateIsProgressPaused()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let _ = component.context.engine.messages.editStoryPrivacy(id: component.slice.item.storyItem.id, privacy: privacy).start()
|
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 {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.openItemPrivacySettings(initialPrivacy: privacy)
|
self.openItemPrivacySettings(updatedPrivacy: privacy)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
editBlockedPeers: { [weak self] privacy, _, _ in
|
editBlockedPeers: { [weak self] privacy, _, _ in
|
||||||
@ -4332,7 +4408,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.openItemPrivacySettings(initialPrivacy: privacy)
|
self.openItemPrivacySettings(updatedPrivacy: privacy)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -4360,11 +4436,12 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
} else if privacy.base == .nobody {
|
} else if privacy.base == .nobody {
|
||||||
subject = .chats(blocked: false)
|
subject = .chats(blocked: false)
|
||||||
} else {
|
} else {
|
||||||
subject = .contacts(privacy.base)
|
subject = .contacts(base: privacy.base)
|
||||||
}
|
}
|
||||||
let stateContext = ShareWithPeersScreen.StateContext(
|
let stateContext = ShareWithPeersScreen.StateContext(
|
||||||
context: context,
|
context: context,
|
||||||
subject: subject,
|
subject: subject,
|
||||||
|
editing: true,
|
||||||
initialPeerIds: Set(privacy.additionallyIncludePeers),
|
initialPeerIds: Set(privacy.additionallyIncludePeers),
|
||||||
blockedPeersContext: component.blockedPeers
|
blockedPeersContext: component.blockedPeers
|
||||||
)
|
)
|
||||||
@ -4737,7 +4814,6 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
|
|
||||||
self.dismissAllTooltips()
|
self.dismissAllTooltips()
|
||||||
|
|
||||||
//TODO:localize
|
|
||||||
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||||
let animationBackgroundColor = presentationData.theme.rootController.tabBar.backgroundColor
|
let animationBackgroundColor = presentationData.theme.rootController.tabBar.backgroundColor
|
||||||
component.presentController(UndoOverlayController(
|
component.presentController(UndoOverlayController(
|
||||||
@ -4750,7 +4826,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
"info2.info2.Fill": animationBackgroundColor
|
"info2.info2.Fill": animationBackgroundColor
|
||||||
],
|
],
|
||||||
title: nil,
|
title: nil,
|
||||||
text: "Subscribe to [Telegram Premium]() to save other people's unprotected stories to your Gallery.",
|
text: component.strings.Story_ToastPremiumSaveToGallery,
|
||||||
customUndoText: nil,
|
customUndoText: nil,
|
||||||
timeout: nil
|
timeout: nil
|
||||||
),
|
),
|
||||||
@ -4788,9 +4864,8 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let context = component.context
|
let context = component.context
|
||||||
//TODO:localize
|
|
||||||
var replaceImpl: ((ViewController) -> Void)?
|
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
|
controller.action = { [weak self] in
|
||||||
guard let self else {
|
guard let self else {
|
||||||
return
|
return
|
||||||
@ -4920,7 +4995,14 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let standaloneReactionAnimation = StandaloneReactionAnimation(genericReactionEffect: nil, useDirectRendering: false)
|
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.frame = self.bounds
|
||||||
standaloneReactionAnimation.animateReactionSelection(
|
standaloneReactionAnimation.animateReactionSelection(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
@ -4937,11 +5019,17 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let standaloneReactionAnimation = self.standaloneReactionAnimation {
|
||||||
|
self.standaloneReactionAnimation = nil
|
||||||
|
standaloneReactionAnimation.view.removeFromSuperview()
|
||||||
|
}
|
||||||
|
self.standaloneReactionAnimation = standaloneReactionAnimation
|
||||||
|
|
||||||
standaloneReactionAnimation.frame = self.bounds
|
standaloneReactionAnimation.frame = self.bounds
|
||||||
self.componentContainerView.addSubnode(standaloneReactionAnimation)
|
self.componentContainerView.addSubview(standaloneReactionAnimation.view)
|
||||||
},
|
},
|
||||||
completion: { [weak standaloneReactionAnimation] in
|
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 {
|
if case let .user(accountUser) = component.slice.peer {
|
||||||
//TODO:localize
|
items.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextStealthMode, icon: { theme in
|
||||||
items.append(.action(ContextMenuActionItem(text: "Stealth Mode", icon: { theme in
|
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: accountUser.isPremium ? "Chat/Context Menu/Eye" : "Chat/Context Menu/EyeLocked"), color: theme.contextMenu.primaryColor)
|
return generateTintedImage(image: UIImage(bundleImageName: accountUser.isPremium ? "Chat/Context Menu/Eye" : "Chat/Context Menu/EyeLocked"), color: theme.contextMenu.primaryColor)
|
||||||
}, action: { [weak self] _, a in
|
}, action: { [weak self] _, a in
|
||||||
a(.default)
|
a(.default)
|
||||||
@ -5437,8 +5524,7 @@ public final class StoryItemSetContainerComponent: Component {
|
|||||||
})))
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO:localize
|
items.append(.action(ContextMenuActionItem(text: component.strings.Story_ContextStealthMode, icon: { theme in
|
||||||
items.append(.action(ContextMenuActionItem(text: "Stealth Mode", icon: { theme in
|
|
||||||
return generateTintedImage(image: UIImage(bundleImageName: accountUser.isPremium ? "Chat/Context Menu/Eye" : "Chat/Context Menu/EyeLocked"), color: theme.contextMenu.primaryColor)
|
return generateTintedImage(image: UIImage(bundleImageName: accountUser.isPremium ? "Chat/Context Menu/Eye" : "Chat/Context Menu/EyeLocked"), color: theme.contextMenu.primaryColor)
|
||||||
}, action: { [weak self] _, a in
|
}, action: { [weak self] _, a in
|
||||||
a(.default)
|
a(.default)
|
||||||
|
@ -503,20 +503,19 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let timestamp = Int32(Date().timeIntervalSince1970)
|
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 theme = component.theme
|
||||||
let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) })
|
let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) })
|
||||||
|
|
||||||
//TODO:localize
|
|
||||||
let alertController = textAlertController(
|
let alertController = textAlertController(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
updatedPresentationData: updatedPresentationData,
|
updatedPresentationData: updatedPresentationData,
|
||||||
title: "You are in Stealth Mode now",
|
title: component.strings.Story_AlertStealthModeActiveTitle,
|
||||||
text: "If you send a reply or reaction, the creator of the story will also see you in the list of viewers.",
|
text: component.strings.Story_AlertStealthModeActiveText,
|
||||||
actions: [
|
actions: [
|
||||||
TextAlertAction(type: .defaultAction, title: "Cancel", action: {}),
|
TextAlertAction(type: .defaultAction, title: component.strings.Common_Cancel, action: {}),
|
||||||
TextAlertAction(type: .genericAction, title: "Proceed", action: {
|
TextAlertAction(type: .genericAction, title: component.strings.Story_AlertStealthModeActiveAction, action: {
|
||||||
action()
|
action()
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
@ -532,6 +531,11 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
view.updateIsProgressPaused()
|
view.updateIsProgressPaused()
|
||||||
|
|
||||||
component.controller()?.presentInGlobalOverlay(alertController)
|
component.controller()?.presentInGlobalOverlay(alertController)
|
||||||
|
|
||||||
|
#if DEBUG
|
||||||
|
#else
|
||||||
|
let _ = ApplicationSpecificNotice.incrementStoryStealthModeReplyCount(accountManager: component.context.sharedContext.accountManager).start()
|
||||||
|
#endif
|
||||||
} else {
|
} else {
|
||||||
action()
|
action()
|
||||||
}
|
}
|
||||||
@ -833,7 +837,12 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
|
|
||||||
self.videoRecorder.set(.single(nil))
|
self.videoRecorder.set(.single(nil))
|
||||||
|
|
||||||
|
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])
|
self.sendMessages(view: view, peer: peer, messages: [updatedMessage])
|
||||||
|
})
|
||||||
}, displaySlowmodeTooltip: { [weak self] view, rect in
|
}, displaySlowmodeTooltip: { [weak self] view, rect in
|
||||||
//self?.interfaceInteraction?.displaySlowmodeTooltip(view, rect)
|
//self?.interfaceInteraction?.displaySlowmodeTooltip(view, rect)
|
||||||
let _ = self
|
let _ = self
|
||||||
@ -878,9 +887,14 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
|
|
||||||
let waveformBuffer: Data? = data.waveform
|
let waveformBuffer: Data? = data.waveform
|
||||||
|
|
||||||
|
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: [])])
|
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()
|
HapticFeedback().tap()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else if let videoRecorderValue = self.videoRecorderValue {
|
} else if let videoRecorderValue = self.videoRecorderValue {
|
||||||
@ -3059,11 +3073,10 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
let remainingActiveSeconds = activeUntilTimestamp - timestamp
|
let remainingActiveSeconds = activeUntilTimestamp - timestamp
|
||||||
|
|
||||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme)
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme)
|
||||||
//TODO:localize
|
let text = component.strings.Story_ToastStealthModeActiveText(timeIntervalString(strings: presentationData.strings, value: remainingActiveSeconds)).string
|
||||||
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 tooltipScreen = UndoOverlayController(
|
let tooltipScreen = UndoOverlayController(
|
||||||
presentationData: presentationData,
|
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,
|
elevatedLayout: false,
|
||||||
animateInAsReplacement: false,
|
animateInAsReplacement: false,
|
||||||
action: { _ in
|
action: { _ in
|
||||||
@ -3087,9 +3100,8 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
let remainingActiveSeconds = max(1, activeUntilTimestamp - timestamp)
|
let remainingActiveSeconds = max(1, activeUntilTimestamp - timestamp)
|
||||||
|
|
||||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme)
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme)
|
||||||
//TODO:localize
|
let text = component.strings.Story_ToastStealthModeActiveText(timeIntervalString(strings: presentationData.strings, value: remainingActiveSeconds)).string
|
||||||
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: component.strings.Story_ToastStealthModeActiveTitle, text: text, cancel: "", destructive: false)
|
||||||
tooltipScreenValue.content = .actionSucceeded(title: "You are in Stealth Mode", text: text, cancel: "", destructive: false)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
self.tooltipScreen?.dismiss(animated: true)
|
self.tooltipScreen?.dismiss(animated: true)
|
||||||
@ -3128,11 +3140,10 @@ final class StoryItemSetContainerSendMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme)
|
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkPresentationTheme)
|
||||||
//TODO:localize
|
let text = component.strings.Story_ToastStealthModeActivatedText(timeIntervalString(strings: presentationData.strings, value: pastPeriod), timeIntervalString(strings: presentationData.strings, value: futurePeriod)).string
|
||||||
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 tooltipScreen = UndoOverlayController(
|
let tooltipScreen = UndoOverlayController(
|
||||||
presentationData: presentationData,
|
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,
|
elevatedLayout: false,
|
||||||
animateInAsReplacement: false,
|
animateInAsReplacement: false,
|
||||||
action: { _ in
|
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 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
|
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(
|
let locationController = LocationViewController(
|
||||||
context: context,
|
context: context,
|
||||||
updatedPresentationData: updatedPresentationData,
|
updatedPresentationData: updatedPresentationData,
|
||||||
|
@ -586,6 +586,9 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
if let baseContentView, baseContentView.configuration == self.configuration, baseContentView.query == nil {
|
if let baseContentView, baseContentView.configuration == self.configuration, baseContentView.query == nil {
|
||||||
parentSource = baseContentView.viewList
|
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)
|
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?
|
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<Empty>
|
let premiumFooterText: ComponentView<Empty>
|
||||||
if let current = self.premiumFooterText {
|
if let current = self.premiumFooterText {
|
||||||
premiumFooterText = current
|
premiumFooterText = current
|
||||||
@ -768,8 +771,15 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
let link = MarkdownAttributeSet(font: Font.semibold(fontSize), textColor: component.theme.list.itemAccentColor)
|
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", "") })
|
let attributes = MarkdownAttributes(body: body, bold: bold, link: link, linkAttribute: { _ in return ("URL", "") })
|
||||||
|
|
||||||
//TODO:localize
|
let text: String
|
||||||
let text = "To unlock viewers' lists for expired and saved stories, subscribe to [Telegram Premium]()."
|
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(
|
premiumFooterSize = premiumFooterText.update(
|
||||||
transition: .immediate,
|
transition: .immediate,
|
||||||
component: AnyComponent(BalancedTextComponent(
|
component: AnyComponent(BalancedTextComponent(
|
||||||
@ -778,14 +788,14 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
maximumNumberOfLines: 0,
|
maximumNumberOfLines: 0,
|
||||||
lineSpacing: 0.2,
|
lineSpacing: 0.2,
|
||||||
highlightColor: component.theme.list.itemAccentColor.withMultipliedAlpha(0.5),
|
highlightColor: component.theme.list.itemAccentColor.withMultipliedAlpha(0.5),
|
||||||
highlightAction: { attributes in
|
highlightAction: component.hasPremium ? nil : { attributes in
|
||||||
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
|
if let _ = attributes[NSAttributedString.Key(rawValue: "URL")] {
|
||||||
return NSAttributedString.Key(rawValue: "URL")
|
return NSAttributedString.Key(rawValue: "URL")
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
tapAction: { [weak self] _, _ in
|
tapAction: component.hasPremium ? nil : { [weak self] _, _ in
|
||||||
guard let self, let component = self.component else {
|
guard let self, let component = self.component else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -793,7 +803,7 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
}
|
}
|
||||||
)),
|
)),
|
||||||
environment: {},
|
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 {
|
} else {
|
||||||
if let premiumFooterText = self.premiumFooterText {
|
if let premiumFooterText = self.premiumFooterText {
|
||||||
@ -895,27 +905,32 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
if self.configuration.listMode == .everyone && (self.query == nil || self.query == "") {
|
if self.configuration.listMode == .everyone && (self.query == nil || self.query == "") {
|
||||||
if component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) {
|
if component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) {
|
||||||
if emptyButton == nil {
|
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 {
|
} else {
|
||||||
//TODO:localize
|
text = component.strings.Story_Views_ViewsExpired
|
||||||
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]()."
|
}
|
||||||
|
} else {
|
||||||
|
text = component.strings.Story_ViewList_PremiumUpgradeText
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
text = component.strings.Story_Views_NoViews
|
text = component.strings.Story_Views_NoViews
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
//TODO:localize
|
|
||||||
if let query = self.query, !query.isEmpty {
|
if let query = self.query, !query.isEmpty {
|
||||||
text = "No views found"
|
text = component.strings.Story_ViewList_EmptyTextSearch
|
||||||
} else if self.configuration.listMode == .contacts {
|
} else if self.configuration.listMode == .contacts {
|
||||||
text = "None of your contacts viewed this story."
|
text = component.strings.Story_ViewList_EmptyTextContacts
|
||||||
} else {
|
} else {
|
||||||
if component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) {
|
if component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) {
|
||||||
if emptyButton == nil {
|
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 {
|
} else {
|
||||||
//TODO:localize
|
text = component.strings.Story_Views_ViewsExpired
|
||||||
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]()."
|
}
|
||||||
|
} else {
|
||||||
|
text = component.strings.Story_ViewList_PremiumUpgradeText
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
text = component.strings.Story_Views_NoViews
|
text = component.strings.Story_Views_NoViews
|
||||||
@ -952,7 +967,6 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
|
|
||||||
var emptyButtonSize: CGSize?
|
var emptyButtonSize: CGSize?
|
||||||
if let emptyButton {
|
if let emptyButton {
|
||||||
//TODO:localize
|
|
||||||
emptyButtonSize = emptyButton.update(
|
emptyButtonSize = emptyButton.update(
|
||||||
transition: emptyButtonTransition,
|
transition: emptyButtonTransition,
|
||||||
component: AnyComponent(ButtonComponent(
|
component: AnyComponent(ButtonComponent(
|
||||||
@ -964,7 +978,7 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
content: AnyComponentWithIdentity(
|
content: AnyComponentWithIdentity(
|
||||||
id: AnyHashable(0),
|
id: AnyHashable(0),
|
||||||
component: AnyComponent(ButtonTextContentComponent(
|
component: AnyComponent(ButtonTextContentComponent(
|
||||||
text: "Learn More",
|
text: component.strings.Story_ViewList_PremiumUpgradeAction,
|
||||||
badge: 0,
|
badge: 0,
|
||||||
textColor: component.theme.list.itemCheckColors.foregroundColor,
|
textColor: component.theme.list.itemCheckColors.foregroundColor,
|
||||||
badgeBackground: 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)
|
let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme)
|
||||||
var items: [ContextMenuItem] = []
|
var items: [ContextMenuItem] = []
|
||||||
|
|
||||||
//TODO:localize
|
|
||||||
let sortMode = self.sortMode
|
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)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Reactions"), color: theme.contextMenu.primaryColor)
|
||||||
}, additionalLeftIcon: { theme in
|
}, additionalLeftIcon: { theme in
|
||||||
if sortMode != .reactionsFirst {
|
if sortMode != .reactionsFirst {
|
||||||
@ -1169,7 +1182,7 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
self.state?.updated(transition: .immediate)
|
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)
|
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Time"), color: theme.contextMenu.primaryColor)
|
||||||
}, additionalLeftIcon: { theme in
|
}, additionalLeftIcon: { theme in
|
||||||
if sortMode != .recentFirst {
|
if sortMode != .recentFirst {
|
||||||
@ -1190,9 +1203,8 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
|
|
||||||
items.append(.separator)
|
items.append(.separator)
|
||||||
|
|
||||||
//TODO:localize
|
|
||||||
let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil
|
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))
|
let contextItems = ContextController.Items(content: .list(items))
|
||||||
|
|
||||||
@ -1233,7 +1245,6 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
|
|
||||||
let visualHeight: CGFloat = max(component.minHeight, component.effectiveHeight)
|
let visualHeight: CGFloat = max(component.minHeight, component.effectiveHeight)
|
||||||
|
|
||||||
//TODO:localize
|
|
||||||
let tabSelectorSize = self.tabSelector.update(
|
let tabSelectorSize = self.tabSelector.update(
|
||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(TabSelectorComponent(
|
component: AnyComponent(TabSelectorComponent(
|
||||||
@ -1244,11 +1255,11 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
items: [
|
items: [
|
||||||
TabSelectorComponent.Item(
|
TabSelectorComponent.Item(
|
||||||
id: AnyHashable(ListMode.everyone.rawValue),
|
id: AnyHashable(ListMode.everyone.rawValue),
|
||||||
title: "All Viewers"
|
title: component.strings.Story_ViewList_TabTitleAll
|
||||||
),
|
),
|
||||||
TabSelectorComponent.Item(
|
TabSelectorComponent.Item(
|
||||||
id: AnyHashable(ListMode.contacts.rawValue),
|
id: AnyHashable(ListMode.contacts.rawValue),
|
||||||
title: "Contacts"
|
title: component.strings.Story_ViewList_TabTitleContacts
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
selectedId: AnyHashable(self.listMode == .everyone ? 0 : 1),
|
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)
|
containerSize: CGSize(width: availableSize.width - 10.0 * 2.0, height: 50.0)
|
||||||
)
|
)
|
||||||
|
|
||||||
//TODO:localize
|
|
||||||
let titleText: String
|
let titleText: String
|
||||||
if let views = component.storyItem.views, views.seenCount != 0 {
|
if let views = component.storyItem.views, views.seenCount != 0 {
|
||||||
if component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) {
|
if component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) {
|
||||||
titleText = component.strings.Story_Footer_Views(Int32(views.seenCount))
|
titleText = component.strings.Story_Footer_Views(Int32(views.seenCount))
|
||||||
} else {
|
} else {
|
||||||
titleText = "Viewers"
|
titleText = component.strings.Story_ViewList_TitleViewers
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
titleText = "No Views"
|
titleText = component.strings.Story_ViewList_TitleEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
let titleSize = self.title.update(
|
let titleSize = self.title.update(
|
||||||
@ -1314,6 +1324,7 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
foreground: .white,
|
foreground: .white,
|
||||||
button: component.theme.rootController.navigationBar.accentTextColor
|
button: component.theme.rootController.navigationBar.accentTextColor
|
||||||
),
|
),
|
||||||
|
cancel: component.strings.Common_Cancel,
|
||||||
placeholder: component.strings.Common_Search,
|
placeholder: component.strings.Common_Search,
|
||||||
isSearchActive: component.isSearchActive,
|
isSearchActive: component.isSearchActive,
|
||||||
collapseFraction: 1.0,
|
collapseFraction: 1.0,
|
||||||
@ -1349,7 +1360,7 @@ final class StoryItemSetViewListComponent: Component {
|
|||||||
|
|
||||||
if !component.hasPremium, component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) {
|
if !component.hasPremium, component.storyItem.expirationTimestamp <= Int32(Date().timeIntervalSince1970) {
|
||||||
} else {
|
} else {
|
||||||
if let views = component.storyItem.views {
|
if let views = component.storyItem.views, views.hasList {
|
||||||
if views.seenCount >= 20 || component.context.sharedContext.immediateExperimentalUISettings.storiesExperiment {
|
if views.seenCount >= 20 || component.context.sharedContext.immediateExperimentalUISettings.storiesExperiment {
|
||||||
displayModeSelector = true
|
displayModeSelector = true
|
||||||
displaySearchBar = true
|
displaySearchBar = true
|
||||||
|
@ -302,7 +302,6 @@ public final class StoryFooterPanelComponent: Component {
|
|||||||
|
|
||||||
self.viewStatsButton.isEnabled = viewCount != 0
|
self.viewStatsButton.isEnabled = viewCount != 0
|
||||||
|
|
||||||
//TODO:localize
|
|
||||||
var regularSegments: [AnimatedCountLabelView.Segment] = []
|
var regularSegments: [AnimatedCountLabelView.Segment] = []
|
||||||
if viewCount != 0 {
|
if viewCount != 0 {
|
||||||
regularSegments.append(.number(viewCount, NSAttributedString(string: "\(viewCount)", font: Font.regular(15.0), textColor: .white)))
|
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
|
let viewPart: String
|
||||||
if viewCount == 0 {
|
if viewCount == 0 {
|
||||||
viewPart = "No Views"
|
viewPart = component.strings.Story_Footer_NoViews
|
||||||
} else if viewCount == 1 {
|
|
||||||
viewPart = " View"
|
|
||||||
} else {
|
} 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))
|
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 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)
|
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)
|
let avatarsNodeFrame = CGRect(origin: CGPoint(x: contentX, y: floor((size.height - avatarsSize.height) * 0.5)), size: avatarsSize)
|
||||||
|
@ -139,8 +139,7 @@ public final class StoryStealthModeInfoContentComponent: Component {
|
|||||||
contentHeight += 15.0
|
contentHeight += 15.0
|
||||||
|
|
||||||
let titleString = NSMutableAttributedString()
|
let titleString = NSMutableAttributedString()
|
||||||
//TODO:localize
|
titleString.append(NSAttributedString(string: component.strings.Story_StealthMode_Title, font: Font.semibold(19.0), textColor: component.theme.list.itemPrimaryTextColor))
|
||||||
titleString.append(NSAttributedString(string: "Stealth Mode", font: Font.semibold(19.0), textColor: component.theme.list.itemPrimaryTextColor))
|
|
||||||
let imageAttachment = NSTextAttachment()
|
let imageAttachment = NSTextAttachment()
|
||||||
imageAttachment.image = self.iconBackground.image
|
imageAttachment.image = self.iconBackground.image
|
||||||
titleString.append(NSAttributedString(attachment: imageAttachment))
|
titleString.append(NSAttributedString(attachment: imageAttachment))
|
||||||
@ -163,13 +162,12 @@ public final class StoryStealthModeInfoContentComponent: Component {
|
|||||||
contentHeight += titleSize.height
|
contentHeight += titleSize.height
|
||||||
contentHeight += 15.0
|
contentHeight += 15.0
|
||||||
|
|
||||||
//TODO:localize
|
|
||||||
let text: String
|
let text: String
|
||||||
switch component.mode {
|
switch component.mode {
|
||||||
case .control:
|
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:
|
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()
|
let mainText = NSMutableAttributedString()
|
||||||
mainText.append(parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(
|
mainText.append(parseMarkdownIntoAttributedString(text, attributes: MarkdownAttributes(
|
||||||
@ -216,17 +214,16 @@ public final class StoryStealthModeInfoContentComponent: Component {
|
|||||||
var title: String
|
var title: String
|
||||||
var text: String
|
var text: String
|
||||||
}
|
}
|
||||||
//TODO:localize
|
|
||||||
let itemDescs: [ItemDesc] = [
|
let itemDescs: [ItemDesc] = [
|
||||||
ItemDesc(
|
ItemDesc(
|
||||||
icon: "Stories/StealthModeIntroIconHidePrevious",
|
icon: "Stories/StealthModeIntroIconHidePrevious",
|
||||||
title: "Hide Recent Views",
|
title: component.strings.Story_StealthMode_RecentTitle,
|
||||||
text: "Hide my views in the last **\(timeIntervalString(strings: component.strings, value: component.backwardDuration))**."
|
text: component.strings.Story_StealthMode_RecentText(timeIntervalString(strings: component.strings, value: component.backwardDuration)).string
|
||||||
),
|
),
|
||||||
ItemDesc(
|
ItemDesc(
|
||||||
icon: "Stories/StealthModeIntroIconHideNext",
|
icon: "Stories/StealthModeIntroIconHideNext",
|
||||||
title: "Hide Next Views",
|
title: component.strings.Story_StealthMode_NextTitle,
|
||||||
text: "Hide my views in the next **\(timeIntervalString(strings: component.strings, value: component.forwardDuration))**."
|
text: component.strings.Story_StealthMode_NextText(timeIntervalString(strings: component.strings, value: component.forwardDuration)).string
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
for i in 0 ..< itemDescs.count {
|
for i in 0 ..< itemDescs.count {
|
||||||
|
@ -137,7 +137,6 @@ private final class StoryStealthModeSheetContentComponent: Component {
|
|||||||
toast = ComponentView()
|
toast = ComponentView()
|
||||||
self.toast = toast
|
self.toast = toast
|
||||||
}
|
}
|
||||||
//TODO:localize
|
|
||||||
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
let body = MarkdownAttributeSet(font: Font.regular(14.0), textColor: .white)
|
||||||
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
let bold = MarkdownAttributeSet(font: Font.semibold(14.0), textColor: .white)
|
||||||
let toastSize = toast.update(
|
let toastSize = toast.update(
|
||||||
@ -149,7 +148,7 @@ private final class StoryStealthModeSheetContentComponent: Component {
|
|||||||
size: CGSize(width: 32.0, height: 32.0)
|
size: CGSize(width: 32.0, height: 32.0)
|
||||||
)),
|
)),
|
||||||
content: AnyComponent(MultilineTextComponent(
|
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
|
maximumNumberOfLines: 0
|
||||||
))
|
))
|
||||||
)),
|
)),
|
||||||
@ -247,13 +246,13 @@ private final class StoryStealthModeSheetContentComponent: Component {
|
|||||||
switch component.mode {
|
switch component.mode {
|
||||||
case .control:
|
case .control:
|
||||||
if remainingCooldownSeconds <= 0 {
|
if remainingCooldownSeconds <= 0 {
|
||||||
buttonText = "Enable Stealth Mode"
|
buttonText = environment.strings.Story_StealthMode_EnableAction
|
||||||
} else {
|
} 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)))
|
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:
|
case .upgrade:
|
||||||
buttonText = "Unlock Stealth Mode"
|
buttonText = environment.strings.Story_StealthMode_UpgradeAction
|
||||||
content = AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(
|
content = AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(
|
||||||
HStack([
|
HStack([
|
||||||
AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(Text(text: buttonText, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor))),
|
AnyComponentWithIdentity(id: AnyHashable(1 as Int), component: AnyComponent(Text(text: buttonText, font: Font.semibold(17.0), color: environment.theme.list.itemCheckColors.foregroundColor))),
|
||||||
|
12
submodules/TelegramUI/Images.xcassets/Media Editor/DownArrow.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Media Editor/DownArrow.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "DownArrow.pdf",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
92
submodules/TelegramUI/Images.xcassets/Media Editor/DownArrow.imageset/DownArrow.pdf
vendored
Normal file
92
submodules/TelegramUI/Images.xcassets/Media Editor/DownArrow.imageset/DownArrow.pdf
vendored
Normal file
@ -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
|
@ -1427,7 +1427,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let updatedImageFrame: CGRect
|
var updatedImageFrame: CGRect
|
||||||
var contextContentFrame: CGRect
|
var contextContentFrame: CGRect
|
||||||
if let _ = emojiString {
|
if let _ = emojiString {
|
||||||
updatedImageFrame = imageFrame
|
updatedImageFrame = imageFrame
|
||||||
@ -1458,7 +1458,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
strongSelf.contextSourceNode.contentRect = contextContentFrame
|
strongSelf.contextSourceNode.contentRect = contextContentFrame
|
||||||
strongSelf.containerNode.targetNodeForActivationProgressContentRect = strongSelf.contextSourceNode.contentRect
|
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?
|
var file: TelegramMediaFile?
|
||||||
if let emojiFile = emojiFile {
|
if let emojiFile = emojiFile {
|
||||||
|
@ -117,6 +117,9 @@ public func navigateToChatControllerImpl(_ params: NavigateToChatControllerParam
|
|||||||
if let attachBotStart = params.attachBotStart {
|
if let attachBotStart = params.attachBotStart {
|
||||||
controller.presentAttachmentBot(botId: attachBotStart.botId, payload: attachBotStart.payload, justInstalled: attachBotStart.justInstalled)
|
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)
|
params.setupController(controller)
|
||||||
found = true
|
found = true
|
||||||
break
|
break
|
||||||
|
@ -1869,8 +1869,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
|||||||
return mediaPickerController(context: context, hasSearch: hasSearch, completion: completion)
|
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 {
|
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)
|
return storyMediaPickerController(context: context, getSourceRect: getSourceRect, completion: completion, dismissed: dismissed, groupsPresented: groupsPresented)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController {
|
public func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController {
|
||||||
|
@ -1075,7 +1075,11 @@ public class TranslateScreen: ViewController {
|
|||||||
self.component = AnyComponent(component)
|
self.component = AnyComponent(component)
|
||||||
self.theme = theme
|
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) {
|
required public init(coder aDecoder: NSCoder) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user