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