Merge commit '5884fdff66c79219ea587650503de82d14487d69' into share-v2

This commit is contained in:
Ali 2023-08-15 00:10:14 +04:00
commit 1faf0a9d39
59 changed files with 1528 additions and 323 deletions

View File

@ -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 wasnt 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$@** wont 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 wasnt 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";

View File

@ -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

View File

@ -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)
}

View File

@ -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
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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

View File

@ -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
}

View File

@ -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
)),

View File

@ -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)

View File

@ -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 {

View File

@ -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))
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
if venue.venue?.id == "city" {
arguments = VenueIconArguments(defaultBackgroundColor: item.presentationData.theme.chat.inputPanel.actionControlFillColor, defaultForegroundColor: .white)
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 {

View File

@ -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

View File

@ -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 ?? ""
}

View File

@ -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
)
}
}

View File

@ -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)

View File

@ -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 {
@ -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:
@ -1528,6 +1632,12 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
}
}
self.titleView.action = { [weak self] in
if let self {
self.presentGroupsMenu()
}
}
self.navigationItem.titleView = self.titleView
if case let .assets(collection, mode) = self.subject, mode != .default {
@ -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)

View File

@ -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()
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)
)

View File

@ -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

View File

@ -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,

View File

@ -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)
}
}
}

View File

@ -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),

View File

@ -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)
}

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 {

View File

@ -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
}
}
}

View File

@ -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 }
@ -3971,6 +3974,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate
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()

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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),

View File

@ -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 {

View File

@ -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)))

View File

@ -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
}
@ -885,23 +900,26 @@ public final class StoryItemSetContainerComponent: Component {
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 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
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
}
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
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)

View File

@ -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: [])])
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()
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))** wont 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,

View File

@ -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

View File

@ -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)

View File

@ -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 {

View File

@ -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))),

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "DownArrow.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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

View File

@ -1427,7 +1427,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
}
}
let updatedImageFrame: CGRect
var updatedImageFrame: CGRect
var contextContentFrame: CGRect
if let _ = emojiString {
updatedImageFrame = imageFrame
@ -1458,7 +1458,10 @@ 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 {

View File

@ -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

View File

@ -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 {

View File

@ -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) {