From d53dd157aaedac34902a65af6a3fbdf8d812476a Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 26 Jul 2023 20:06:39 +0200 Subject: [PATCH 1/6] Cherry-pick fixes --- .../Telegram-iOS/en.lproj/Localizable.strings | 10 + submodules/Camera/Sources/VideoRecorder.swift | 4 +- .../Display/Source/ContainerViewLayout.swift | 10 + .../Sources/DrawingEntitiesView.swift | 2 +- .../Sources/DrawingEntitySnapTool.swift | 6 +- .../DrawingUI/Sources/DrawingScreen.swift | 2 - .../Sources/DrawingStickerEntity.swift | 9 +- .../DrawingUI/Sources/DrawingTextEntity.swift | 46 +- .../Sources/TextSettingsComponent.swift | 3 +- .../Sources/LimitsPageComponent.swift | 585 ++++++++++++++++++ .../Sources/PremiumLimitsListScreen.swift | 573 ----------------- .../PendingMessageUploadedContent.swift | 20 +- .../PendingMessages/RequestEditMessage.swift | 2 +- .../Messages/PendingStoryManager.swift | 1 - .../TelegramEngine/Messages/Stories.swift | 3 +- .../CameraScreen/Sources/CameraScreen.swift | 29 +- .../MetalResources/EditorAdjustments.metal | 2 +- .../MediaEditor/Sources/MediaEditor.swift | 151 ----- .../Sources/MediaEditorComposer.swift | 25 +- .../Sources/MediaEditorComposerEntity.swift | 36 +- .../Sources/MediaEditorRenderChain.swift | 145 +++++ .../Sources/MediaEditorVideoExport.swift | 6 +- .../Components/MediaEditorScreen/BUILD | 2 +- .../Sources/MediaEditorScreen.swift | 205 +----- .../Sources/SaveProgressScreen.swift | 2 - .../Sources/SectionHeaderComponent.swift | 38 +- .../Sources/ShareWithPeersScreen.swift | 309 ++++++++- .../Sources/StoryChatContent.swift | 4 +- .../StoryItemSetContainerComponent.swift | 2 +- .../Sources/EditableTokenListNode.swift | 2 +- .../TelegramUI/Sources/ChatController.swift | 2 +- 31 files changed, 1224 insertions(+), 1012 deletions(-) create mode 100644 submodules/PremiumUI/Sources/LimitsPageComponent.swift create mode 100644 submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderChain.swift diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 04c8b603b0..89e42da1a3 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9558,6 +9558,7 @@ Sorry for the inconvenience."; "Story.PrivacyTooltipContacts" = "This story is shown to all your contacts."; "Story.PrivacyTooltipCloseFriends" = "This story is shown to your close friends."; "Story.PrivacyTooltipSelectedContacts" = "This story is shown to selected contacts."; +"Story.PrivacyTooltipSelectedContactsCount" = "This story is now shown to %@ contacts."; "Story.PrivacyTooltipNobody" = "This story is shown only to you."; "Story.PrivacyTooltipEveryone" = "This story is shown to everyone."; @@ -9728,3 +9729,12 @@ Sorry for the inconvenience."; "Story.Editor.TooltipPremiumCaptionEntities" = "Subscribe to [Telegram Premium]() to add links and formatting in captions to your stories."; "Story.Context.TooltipPremiumSaveStories" = "Subscribe to [Telegram Premium]() to save other people's unprotected stories to your Gallery."; + +"Story.Privacy.GroupTooLarge" = "Group Too Large"; +"Story.Privacy.GroupParticipantsLimit" = "You can select groups that are up to 200 members."; + +"ChatList.StoryFeedTooltipUsers" = "Tap above to view stories from %@"; + +"Story.TooltipPrivacyCloseFriends2" = "You are seeing this story because **%@** added you to their list of Close Friends."; + +"Story.Editor.VideoTooShort" = "A video must be at least 1 second long."; diff --git a/submodules/Camera/Sources/VideoRecorder.swift b/submodules/Camera/Sources/VideoRecorder.swift index 48dd5a2c4f..67c3cb0661 100644 --- a/submodules/Camera/Sources/VideoRecorder.swift +++ b/submodules/Camera/Sources/VideoRecorder.swift @@ -310,8 +310,8 @@ private final class VideoRecorderImpl { self.queue.async { var stopTime = CMTime(seconds: CACurrentMediaTime(), preferredTimescale: CMTimeScale(NSEC_PER_SEC)) if self.recordingStartSampleTime.isValid { - if (stopTime - self.recordingStartSampleTime).seconds < 1.0 { - stopTime = self.recordingStartSampleTime + CMTime(seconds: 1.0, preferredTimescale: self.recordingStartSampleTime.timescale) + if (stopTime - self.recordingStartSampleTime).seconds < 1.5 { + stopTime = self.recordingStartSampleTime + CMTime(seconds: 1.5, preferredTimescale: self.recordingStartSampleTime.timescale) } } diff --git a/submodules/Display/Source/ContainerViewLayout.swift b/submodules/Display/Source/ContainerViewLayout.swift index 5a73047f18..09552bbdae 100644 --- a/submodules/Display/Source/ContainerViewLayout.swift +++ b/submodules/Display/Source/ContainerViewLayout.swift @@ -35,6 +35,16 @@ public struct LayoutMetrics: Equatable { } } +public extension LayoutMetrics { + var isTablet: Bool { + if case .regular = self.widthClass { + return true + } else { + return false + } + } +} + public enum LayoutOrientation { case portrait case landscape diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 7d98f514e2..0dd5519c5f 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -651,7 +651,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { if let selectionView = entityView.makeSelectionView() { selectionView.tapped = { [weak self, weak entityView] in - if let self, let entityView = entityView { + if let self, let entityView { let entityViews = self.subviews.filter { $0 is DrawingEntityView } self.requestedMenuForEntityView(entityView, entityViews.last === entityView) } diff --git a/submodules/DrawingUI/Sources/DrawingEntitySnapTool.swift b/submodules/DrawingUI/Sources/DrawingEntitySnapTool.swift index 583b5ba6fc..7a5e065ac1 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitySnapTool.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitySnapTool.swift @@ -308,7 +308,7 @@ class DrawingEntitySnapTool { func maybeSkipFromStart(entityView: DrawingEntityView, rotation: CGFloat) { self.rotationState = nil - let snapDelta: CGFloat = 0.25 + let snapDelta: CGFloat = 0.01 for snapRotation in self.snapRotations { let snapRotation = snapRotation * .pi if rotation > snapRotation - snapDelta && rotation < snapRotation + snapDelta { @@ -318,7 +318,7 @@ class DrawingEntitySnapTool { } } - func update(entityView: DrawingEntityView, velocity: CGFloat, delta: CGFloat, updatedRotation: CGFloat) -> CGFloat { + func update(entityView: DrawingEntityView, velocity: CGFloat, delta: CGFloat, updatedRotation: CGFloat, skipMultiplier: CGFloat = 1.0) -> CGFloat { var updatedRotation = updatedRotation if updatedRotation < 0.0 { updatedRotation = 2.0 * .pi + updatedRotation @@ -332,7 +332,7 @@ class DrawingEntitySnapTool { let snapDelta: CGFloat = 0.01 let snapVelocity: CGFloat = snapDelta * 35.0 - let snapSkipRotation: CGFloat = snapDelta * 40.0 + let snapSkipRotation: CGFloat = snapDelta * 45.0 * skipMultiplier if abs(velocity) < snapVelocity || self.rotationState?.waitForLeave == true { if let (snapRotation, skipped, waitForLeave) = self.rotationState { diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index b23a9610c6..7c92b04869 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -2978,8 +2978,6 @@ public final class DrawingToolsInteraction { private var isActive = false private var validLayout: ContainerViewLayout? - private let startTimestamp = CACurrentMediaTime() - public init( context: AccountContext, drawingView: DrawingView, diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift index 7e8528c3d3..1d60e7a1ce 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift @@ -547,9 +547,12 @@ final class DrawingStickerEntititySelectionView: DrawingEntitySelectionView { } else { newAngle = atan2(parentLocation.y - self.center.y, parentLocation.x - self.center.x) } - - // let delta = newAngle - updatedRotation - updatedRotation = newAngle// self.snapTool.update(entityView: entityView, velocity: 0.0, delta: delta, updatedRotation: newAngle) + var delta = newAngle - updatedRotation + if delta < -.pi { + delta = 2.0 * .pi + delta + } + let velocityValue = sqrt(velocity.x * velocity.x + velocity.y * velocity.y) / 1000.0 + updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocityValue, delta: delta, updatedRotation: newAngle, skipMultiplier: 1.0) } else if self.currentHandle === self.layer { updatedPosition.x += delta.x updatedPosition.y += delta.y diff --git a/submodules/DrawingUI/Sources/DrawingTextEntity.swift b/submodules/DrawingUI/Sources/DrawingTextEntity.swift index 08d69a7d98..205ecc7a85 100644 --- a/submodules/DrawingUI/Sources/DrawingTextEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingTextEntity.swift @@ -58,13 +58,9 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate self.textView.delegate = self self.addSubview(self.textView) - self.emojiViewProvider = { [weak self] emoji in - guard let strongSelf = self else { - return UIView() - } - + self.emojiViewProvider = { emoji in let pointSize: CGFloat = 128.0 - return EmojiTextAttachmentView(context: context, userLocation: .other, emoji: emoji, file: emoji.file, cache: strongSelf.context.animationCache, renderer: strongSelf.context.animationRenderer, placeholderColor: UIColor.white.withAlphaComponent(0.12), pointSize: CGSize(width: pointSize, height: pointSize)) + return EmojiTextAttachmentView(context: context, userLocation: .other, emoji: emoji, file: emoji.file, cache: context.animationCache, renderer: context.animationRenderer, placeholderColor: UIColor.white.withAlphaComponent(0.12), pointSize: CGSize(width: pointSize, height: pointSize)) } self.textView.onPaste = { [weak self] in @@ -285,9 +281,10 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate } self._isEditing = false - self.textView.resignFirstResponder() self.textView.inputView = nil self.textView.inputAccessoryView = nil + self.textView.reloadInputViews() + self.textView.resignFirstResponder() self.textView.isEditable = false self.textView.isSelectable = false @@ -656,8 +653,8 @@ public final class DrawingTextEntityView: DrawingEntityView, UITextViewDelegate self.updateEditingPosition(animated: animated) } - self.textView.onLayoutUpdate = { - self.updateEntities() + self.textView.onLayoutUpdate = { [weak self] in + self?.updateEntities() } super.update(animated: animated) @@ -872,9 +869,12 @@ final class DrawingTextEntititySelectionView: DrawingEntitySelectionView { } else { newAngle = atan2(parentLocation.y - self.center.y, parentLocation.x - self.center.x) } - - //let delta = newAngle - updatedRotation - updatedRotation = newAngle //" self.snapTool.update(entityView: entityView, velocity: 0.0, delta: delta, updatedRotation: newAngle) + var delta = newAngle - updatedRotation + if delta < -.pi { + delta = 2.0 * .pi + delta + } + let velocityValue = sqrt(velocity.x * velocity.x + velocity.y * velocity.y) / 1000.0 + updatedRotation = self.snapTool.update(entityView: entityView, velocity: velocityValue, delta: delta, updatedRotation: newAngle, skipMultiplier: 1.0) } else if self.currentHandle === self.layer { updatedPosition.x += delta.x updatedPosition.y += delta.y @@ -1023,15 +1023,20 @@ private class DrawingTextLayoutManager: NSLayoutManager { private func prepare() { self.path = nil self.rectArray.removeAll() - + self.enumerateLineFragments(forGlyphRange: NSRange(location: 0, length: ((self.textStorage?.string ?? "") as NSString).length)) { rect, usedRect, textContainer, glyphRange, _ in var ignoreRange = false - let charecterRange = self.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil) - let substring = ((self.textStorage?.string ?? "") as NSString).substring(with: charecterRange) + let characterRange = self.characterRange(forGlyphRange: glyphRange, actualGlyphRange: nil) + let substring = ((self.textStorage?.string ?? "") as NSString).substring(with: characterRange) if substring.trimmingCharacters(in: .newlines).isEmpty { ignoreRange = true } + var usedRect = usedRect + if substring.hasSuffix(" ") { + usedRect.size.width -= floorToScreenPixels(usedRect.height * 0.145) + } + if !ignoreRange { let newRect = CGRect(origin: CGPoint(x: usedRect.minX - self.frameWidthInset, y: usedRect.minY), size: CGSize(width: usedRect.width + self.frameWidthInset * 2.0, height: usedRect.height)) self.rectArray.append(newRect) @@ -1062,15 +1067,16 @@ private class DrawingTextLayoutManager: NSLayoutManager { self.radius = cur.height * 0.18 - let t1 = ((cur.minX - last.minX < 2.0 * self.radius) && (cur.minX > last.minX)) || ((cur.maxX - last.maxX > -2.0 * self.radius) && (cur.maxX < last.maxX)) - let t2 = ((last.minX - cur.minX < 2.0 * self.radius) && (last.minX > cur.minX)) || ((last.maxX - cur.maxX > -2.0 * self.radius) && (last.maxX < cur.maxX)) + let doubleRadius = self.radius * 2.5 + + let t1 = ((cur.minX - last.minX < doubleRadius) && (cur.minX > last.minX)) || ((cur.maxX - last.maxX > -doubleRadius) && (cur.maxX < last.maxX)) + let t2 = ((last.minX - cur.minX < doubleRadius) && (last.minX > cur.minX)) || ((last.maxX - cur.maxX > -doubleRadius) && (last.maxX < cur.maxX)) if t2 { let newRect = CGRect(origin: CGPoint(x: cur.minX, y: last.minY), size: CGSize(width: cur.width, height: last.height)) self.rectArray[index - 1] = newRect self.processRectIndex(index - 1) - } - if t1 { + } else if t1 { let newRect = CGRect(origin: CGPoint(x: last.minX, y: cur.minY), size: CGSize(width: last.width, height: cur.height)) self.rectArray[index] = newRect self.processRectIndex(index + 1) @@ -1124,7 +1130,7 @@ private class DrawingTextLayoutManager: NSLayoutManager { path.append(UIBezierPath(roundedRect: cur, cornerRadius: self.radius)) if i == 0 { last = cur - } else if i > 0 && abs(last.maxY - cur.minY) < 10.0 { + } else if i > 0 && abs(last.maxY - cur.minY) < 15.0 { let a = cur.origin let b = CGPoint(x: cur.maxX, y: cur.minY) let c = CGPoint(x: last.minX, y: last.maxY) diff --git a/submodules/DrawingUI/Sources/TextSettingsComponent.swift b/submodules/DrawingUI/Sources/TextSettingsComponent.swift index 1c803491b3..5702b3b372 100644 --- a/submodules/DrawingUI/Sources/TextSettingsComponent.swift +++ b/submodules/DrawingUI/Sources/TextSettingsComponent.swift @@ -579,12 +579,13 @@ final class TextSettingsComponent: CombinedComponent { ) } + let presentFontPicker = component.presentFontPicker let font = font.update( component: TextFontComponent( selectedValue: component.font, tag: component.fontTag, tapped: { - component.presentFontPicker() + presentFontPicker() } ), availableSize: CGSize(width: fontAvailableWidth, height: 30.0), diff --git a/submodules/PremiumUI/Sources/LimitsPageComponent.swift b/submodules/PremiumUI/Sources/LimitsPageComponent.swift new file mode 100644 index 0000000000..9002cf506f --- /dev/null +++ b/submodules/PremiumUI/Sources/LimitsPageComponent.swift @@ -0,0 +1,585 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import SwiftSignalKit +import TelegramCore +import AccountContext +import MultilineTextComponent +import BlurredBackgroundComponent +import Markdown +import TelegramPresentationData + +private final class LimitComponent: CombinedComponent { + let title: String + let titleColor: UIColor + let text: String + let textColor: UIColor + let accentColor: UIColor + let inactiveColor: UIColor + let inactiveTextColor: UIColor + let inactiveTitle: String + let inactiveValue: String + let activeColor: UIColor + let activeTextColor: UIColor + let activeTitle: String + let activeValue: String + + public init( + title: String, + titleColor: UIColor, + text: String, + textColor: UIColor, + accentColor: UIColor, + inactiveColor: UIColor, + inactiveTextColor: UIColor, + inactiveTitle: String, + inactiveValue: String, + activeColor: UIColor, + activeTextColor: UIColor, + activeTitle: String, + activeValue: String + ) { + self.title = title + self.titleColor = titleColor + self.text = text + self.textColor = textColor + self.accentColor = accentColor + self.inactiveColor = inactiveColor + self.inactiveTextColor = inactiveTextColor + self.inactiveTitle = inactiveTitle + self.inactiveValue = inactiveValue + self.activeColor = activeColor + self.activeTextColor = activeTextColor + self.activeTitle = activeTitle + self.activeValue = activeValue + } + + static func ==(lhs: LimitComponent, rhs: LimitComponent) -> Bool { + if lhs.title != rhs.title { + return false + } + if lhs.titleColor != rhs.titleColor { + return false + } + if lhs.text != rhs.text { + return false + } + if lhs.textColor != rhs.textColor { + return false + } + if lhs.accentColor != rhs.accentColor { + return false + } + if lhs.inactiveColor != rhs.inactiveColor { + return false + } + if lhs.inactiveTextColor != rhs.inactiveTextColor { + return false + } + if lhs.inactiveTitle != rhs.inactiveTitle { + return false + } + if lhs.inactiveValue != rhs.inactiveValue { + return false + } + if lhs.activeColor != rhs.activeColor { + return false + } + if lhs.activeTextColor != rhs.activeTextColor { + return false + } + if lhs.activeTitle != rhs.activeTitle { + return false + } + if lhs.activeValue != rhs.activeValue { + return false + } + return true + } + + static var body: Body { + let title = Child(MultilineTextComponent.self) + let text = Child(MultilineTextComponent.self) + let limit = Child(PremiumLimitDisplayComponent.self) + + return { context in + let component = context.component + + let sideInset: CGFloat = 16.0 + let textSideInset: CGFloat = sideInset + 8.0 + let spacing: CGFloat = 4.0 + + let textTopInset: CGFloat = 9.0 + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString( + string: component.title, + font: Font.regular(17.0), + textColor: component.titleColor, + paragraphAlignment: .natural + )), + horizontalAlignment: .center, + maximumNumberOfLines: 1 + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude), + transition: .immediate + ) + + let textFont = Font.regular(13.0) + let boldTextFont = Font.semibold(13.0) + let textColor = component.textColor + let markdownAttributes = MarkdownAttributes( + body: MarkdownAttributeSet(font: textFont, textColor: textColor), + bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), + link: MarkdownAttributeSet(font: textFont, textColor: component.accentColor), + linkAttribute: { _ in + return nil + } + ) + + let text = text.update( + component: MultilineTextComponent( + text: .markdown(text: component.text, attributes: markdownAttributes), + horizontalAlignment: .natural, + maximumNumberOfLines: 0, + lineSpacing: 0.0 + ), + availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + + let limit = limit.update( + component: PremiumLimitDisplayComponent( + inactiveColor: component.inactiveColor, + activeColors: [component.activeColor], + inactiveTitle: component.inactiveTitle, + inactiveValue: component.inactiveValue, + inactiveTitleColor: component.inactiveTextColor, + activeTitle: component.activeTitle, + activeValue: component.activeValue, + activeTitleColor: component.activeTextColor, + badgeIconName: "", + badgeText: nil, + badgePosition: 0.0, + badgeGraphPosition: 0.5, + isPremiumDisabled: false + ), + availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), + transition: .immediate + ) + + context.add(title + .position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) + ) + + context.add(text + .position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0)) + ) + + context.add(limit + .position(CGPoint(x: context.availableSize.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height - 20.0)) + ) + + return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 56.0) + } + } +} + +private enum Limit: CaseIterable { + case groups + case pins + case publicLinks + case savedGifs + case favedStickers + case about + case captions + case folders + case chatsPerFolder + case account + + func title(strings: PresentationStrings) -> String { + switch self { + case .groups: + return strings.Premium_Limits_GroupsAndChannels + case .pins: + return strings.Premium_Limits_PinnedChats + case .publicLinks: + return strings.Premium_Limits_PublicLinks + case .savedGifs: + return strings.Premium_Limits_SavedGifs + case .favedStickers: + return strings.Premium_Limits_FavedStickers + case .about: + return strings.Premium_Limits_Bio + case .captions: + return strings.Premium_Limits_Captions + case .folders: + return strings.Premium_Limits_Folders + case .chatsPerFolder: + return strings.Premium_Limits_ChatsPerFolder + case .account: + return strings.Premium_Limits_Accounts + } + } + + func text(strings: PresentationStrings) -> String { + switch self { + case .groups: + return strings.Premium_Limits_GroupsAndChannelsInfo + case .pins: + return strings.Premium_Limits_PinnedChatsInfo + case .publicLinks: + return strings.Premium_Limits_PublicLinksInfo + case .savedGifs: + return strings.Premium_Limits_SavedGifsInfo + case .favedStickers: + return strings.Premium_Limits_FavedStickersInfo + case .about: + return strings.Premium_Limits_BioInfo + case .captions: + return strings.Premium_Limits_CaptionsInfo + case .folders: + return strings.Premium_Limits_FoldersInfo + case .chatsPerFolder: + return strings.Premium_Limits_ChatsPerFolderInfo + case .account: + return strings.Premium_Limits_AccountsInfo + } + } + + func limit(_ configuration: EngineConfiguration.UserLimits, isPremium: Bool) -> String { + let value: Int32 + switch self { + case .groups: + value = configuration.maxChannelsCount + case .pins: + value = configuration.maxPinnedChatCount + case .publicLinks: + value = configuration.maxPublicLinksCount + case .savedGifs: + value = configuration.maxSavedGifCount + case .favedStickers: + value = configuration.maxFavedStickerCount + case .about: + value = configuration.maxAboutLength + case .captions: + value = configuration.maxCaptionLength + case .folders: + value = configuration.maxFoldersCount + case .chatsPerFolder: + value = configuration.maxFolderChatsCount + case .account: + value = isPremium ? 4 : 3 + } + return "\(value)" + } +} + +private final class LimitsListComponent: CombinedComponent { + typealias EnvironmentType = (Empty, ScrollChildEnvironment) + + let context: AccountContext + let topInset: CGFloat + let bottomInset: CGFloat + + init(context: AccountContext, topInset: CGFloat, bottomInset: CGFloat) { + self.context = context + self.topInset = topInset + self.bottomInset = bottomInset + } + + static func ==(lhs: LimitsListComponent, rhs: LimitsListComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.topInset != rhs.topInset { + return false + } + if lhs.bottomInset != rhs.bottomInset { + return false + } + return true + } + + final class State: ComponentState { + private let context: AccountContext + + private var disposable: Disposable? + var limits: EngineConfiguration.UserLimits = .defaultValue + var premiumLimits: EngineConfiguration.UserLimits = .defaultValue + + init(context: AccountContext) { + self.context = context + + super.init() + + self.disposable = (context.engine.data.get( + TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), + TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) + ) + |> deliverOnMainQueue).start(next: { [weak self] limits, premiumLimits in + if let strongSelf = self { + strongSelf.limits = limits + strongSelf.premiumLimits = premiumLimits + strongSelf.updated(transition: .immediate) + } + }) + } + + deinit { + self.disposable?.dispose() + } + } + + func makeState() -> State { + return State(context: self.context) + } + + static var body: Body { + let list = Child(List.self) + + return { context in + let state = context.state + let theme = context.component.context.sharedContext.currentPresentationData.with { $0 }.theme + let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings + + let colors = [ + UIColor(rgb: 0x5ba0ff), + UIColor(rgb: 0x798aff), + UIColor(rgb: 0x9377ff), + UIColor(rgb: 0xac64f3), + UIColor(rgb: 0xc456ae), + UIColor(rgb: 0xcf579a), + UIColor(rgb: 0xdb5887), + UIColor(rgb: 0xdb496f), + UIColor(rgb: 0xe95d44), + UIColor(rgb: 0xf2822a) + ] + + let items: [AnyComponentWithIdentity] = Limit.allCases.enumerated().map { index, value in + AnyComponentWithIdentity( + id: value, component: AnyComponent( + LimitComponent( + title: value.title(strings: strings), + titleColor: theme.list.itemPrimaryTextColor, + text: value.text(strings: strings), + textColor: theme.list.itemSecondaryTextColor, + accentColor: theme.list.itemAccentColor, + inactiveColor: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5), + inactiveTextColor: theme.list.itemPrimaryTextColor, + inactiveTitle: strings.Premium_Free, + inactiveValue: value.limit(state.limits, isPremium: false), + activeColor: colors[index], + activeTextColor: .white, + activeTitle: strings.Premium_Premium, + activeValue: value.limit(state.premiumLimits, isPremium: true) + ) + ) + ) + } + + let list = list.update( + component: List(items), + availableSize: CGSize(width: context.availableSize.width, height: 10000.0), + transition: context.transition + ) + + let contentHeight = context.component.topInset + list.size.height + context.component.bottomInset + context.add(list + .position(CGPoint(x: list.size.width / 2.0, y: context.component.topInset + list.size.height / 2.0)) + ) + + return CGSize(width: context.availableSize.width, height: contentHeight) + } + } +} + + +final class LimitsPageComponent: CombinedComponent { + typealias EnvironmentType = DemoPageEnvironment + + let context: AccountContext + let bottomInset: CGFloat + let updatedBottomAlpha: (CGFloat) -> Void + let updatedDismissOffset: (CGFloat) -> Void + let updatedIsDisplaying: (Bool) -> Void + + init(context: AccountContext, bottomInset: CGFloat, updatedBottomAlpha: @escaping (CGFloat) -> Void, updatedDismissOffset: @escaping (CGFloat) -> Void, updatedIsDisplaying: @escaping (Bool) -> Void) { + self.context = context + self.bottomInset = bottomInset + self.updatedBottomAlpha = updatedBottomAlpha + self.updatedDismissOffset = updatedDismissOffset + self.updatedIsDisplaying = updatedIsDisplaying + } + + static func ==(lhs: LimitsPageComponent, rhs: LimitsPageComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.bottomInset != rhs.bottomInset { + return false + } + return true + } + + final class State: ComponentState { + let updateBottomAlpha: (CGFloat) -> Void + let updateDismissOffset: (CGFloat) -> Void + let updatedIsDisplaying: (Bool) -> Void + + var resetScroll: ActionSlot? + + var topContentOffset: CGFloat = 0.0 + var bottomContentOffset: CGFloat = 100.0 { + didSet { + self.updateAlpha() + } + } + + var position: CGFloat? { + didSet { + self.updateAlpha() + } + } + + var isDisplaying = false { + didSet { + if oldValue != self.isDisplaying { + self.updatedIsDisplaying(self.isDisplaying) + + if !self.isDisplaying { + self.resetScroll?.invoke(Void()) + } + } + } + } + + init(updateBottomAlpha: @escaping (CGFloat) -> Void, updateDismissOffset: @escaping (CGFloat) -> Void, updateIsDisplaying: @escaping (Bool) -> Void) { + self.updateBottomAlpha = updateBottomAlpha + self.updateDismissOffset = updateDismissOffset + self.updatedIsDisplaying = updateIsDisplaying + + super.init() + } + + func updateAlpha() { + let dismissPosition = min(1.0, abs(self.position ?? 0.0) / 1.3333) + let position = min(1.0, abs(self.position ?? 0.0)) + self.updateDismissOffset(dismissPosition) + + let verticalPosition = 1.0 - min(30.0, self.bottomContentOffset) / 30.0 + + let backgroundAlpha: CGFloat = max(position, verticalPosition) + self.updateBottomAlpha(backgroundAlpha) + } + } + + func makeState() -> State { + return State(updateBottomAlpha: self.updatedBottomAlpha, updateDismissOffset: self.updatedDismissOffset, updateIsDisplaying: self.updatedIsDisplaying) + } + + static var body: Body { + let background = Child(Rectangle.self) + let scroll = Child(ScrollComponent.self) + let topPanel = Child(BlurredBackgroundComponent.self) + let topSeparator = Child(Rectangle.self) + let title = Child(MultilineTextComponent.self) + + let resetScroll = ActionSlot() + + return { context in + let state = context.state + + let environment = context.environment[DemoPageEnvironment.self].value + state.resetScroll = resetScroll + state.position = environment.position + state.isDisplaying = environment.isDisplaying + + let theme = context.component.context.sharedContext.currentPresentationData.with { $0 }.theme + let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings + + let topInset: CGFloat = 56.0 + + let scroll = scroll.update( + component: ScrollComponent( + content: AnyComponent( + LimitsListComponent( + context: context.component.context, + topInset: topInset, + bottomInset: context.component.bottomInset + ) + ), + contentInsets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0), + contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in + state?.topContentOffset = topContentOffset + state?.bottomContentOffset = bottomContentOffset + Queue.mainQueue().justDispatch { + state?.updated(transition: .immediate) + } + }, + contentOffsetWillCommit: { _ in }, + resetScroll: resetScroll + ), + availableSize: context.availableSize, + transition: context.transition + ) + + let background = background.update( + component: Rectangle(color: theme.list.plainBackgroundColor), + availableSize: scroll.size, + transition: context.transition + ) + context.add(background + .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) + ) + + context.add(scroll + .position(CGPoint(x: context.availableSize.width / 2.0, y: scroll.size.height / 2.0)) + ) + + let topPanel = topPanel.update( + component: BlurredBackgroundComponent( + color: theme.rootController.navigationBar.blurredBackgroundColor + ), + availableSize: CGSize(width: context.availableSize.width, height: topInset), + transition: context.transition + ) + + let topSeparator = topSeparator.update( + component: Rectangle( + color: theme.rootController.navigationBar.separatorColor + ), + availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel), + transition: context.transition + ) + + let title = title.update( + component: MultilineTextComponent( + text: .plain(NSAttributedString(string: strings.Premium_DoubledLimits, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)), + horizontalAlignment: .center, + truncationType: .end, + maximumNumberOfLines: 1 + ), + availableSize: context.availableSize, + transition: context.transition + ) + + let topPanelAlpha: CGFloat = min(30.0, state.topContentOffset) / 30.0 + context.add(topPanel + .position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0)) + .opacity(topPanelAlpha) + ) + context.add(topSeparator + .position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height)) + .opacity(topPanelAlpha) + ) + context.add(title + .position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0)) + ) + + return scroll.size + } + } +} diff --git a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift index b4ae52546e..ce43abcc7c 100644 --- a/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumLimitsListScreen.swift @@ -17,579 +17,6 @@ import Markdown import SolidRoundedButtonNode import BlurredBackgroundComponent -private final class LimitComponent: CombinedComponent { - let title: String - let titleColor: UIColor - let text: String - let textColor: UIColor - let accentColor: UIColor - let inactiveColor: UIColor - let inactiveTextColor: UIColor - let inactiveTitle: String - let inactiveValue: String - let activeColor: UIColor - let activeTextColor: UIColor - let activeTitle: String - let activeValue: String - - public init( - title: String, - titleColor: UIColor, - text: String, - textColor: UIColor, - accentColor: UIColor, - inactiveColor: UIColor, - inactiveTextColor: UIColor, - inactiveTitle: String, - inactiveValue: String, - activeColor: UIColor, - activeTextColor: UIColor, - activeTitle: String, - activeValue: String - ) { - self.title = title - self.titleColor = titleColor - self.text = text - self.textColor = textColor - self.accentColor = accentColor - self.inactiveColor = inactiveColor - self.inactiveTextColor = inactiveTextColor - self.inactiveTitle = inactiveTitle - self.inactiveValue = inactiveValue - self.activeColor = activeColor - self.activeTextColor = activeTextColor - self.activeTitle = activeTitle - self.activeValue = activeValue - } - - static func ==(lhs: LimitComponent, rhs: LimitComponent) -> Bool { - if lhs.title != rhs.title { - return false - } - if lhs.titleColor != rhs.titleColor { - return false - } - if lhs.text != rhs.text { - return false - } - if lhs.textColor != rhs.textColor { - return false - } - if lhs.accentColor != rhs.accentColor { - return false - } - if lhs.inactiveColor != rhs.inactiveColor { - return false - } - if lhs.inactiveTextColor != rhs.inactiveTextColor { - return false - } - if lhs.inactiveTitle != rhs.inactiveTitle { - return false - } - if lhs.inactiveValue != rhs.inactiveValue { - return false - } - if lhs.activeColor != rhs.activeColor { - return false - } - if lhs.activeTextColor != rhs.activeTextColor { - return false - } - if lhs.activeTitle != rhs.activeTitle { - return false - } - if lhs.activeValue != rhs.activeValue { - return false - } - return true - } - - static var body: Body { - let title = Child(MultilineTextComponent.self) - let text = Child(MultilineTextComponent.self) - let limit = Child(PremiumLimitDisplayComponent.self) - - return { context in - let component = context.component - - let sideInset: CGFloat = 16.0 - let textSideInset: CGFloat = sideInset + 8.0 - let spacing: CGFloat = 4.0 - - let textTopInset: CGFloat = 9.0 - - let title = title.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString( - string: component.title, - font: Font.regular(17.0), - textColor: component.titleColor, - paragraphAlignment: .natural - )), - horizontalAlignment: .center, - maximumNumberOfLines: 1 - ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: CGFloat.greatestFiniteMagnitude), - transition: .immediate - ) - - let textFont = Font.regular(13.0) - let boldTextFont = Font.semibold(13.0) - let textColor = component.textColor - let markdownAttributes = MarkdownAttributes( - body: MarkdownAttributeSet(font: textFont, textColor: textColor), - bold: MarkdownAttributeSet(font: boldTextFont, textColor: textColor), - link: MarkdownAttributeSet(font: textFont, textColor: component.accentColor), - linkAttribute: { _ in - return nil - } - ) - - let text = text.update( - component: MultilineTextComponent( - text: .markdown(text: component.text, attributes: markdownAttributes), - horizontalAlignment: .natural, - maximumNumberOfLines: 0, - lineSpacing: 0.0 - ), - availableSize: CGSize(width: context.availableSize.width - textSideInset * 2.0, height: context.availableSize.height), - transition: .immediate - ) - - let limit = limit.update( - component: PremiumLimitDisplayComponent( - inactiveColor: component.inactiveColor, - activeColors: [component.activeColor], - inactiveTitle: component.inactiveTitle, - inactiveValue: component.inactiveValue, - inactiveTitleColor: component.inactiveTextColor, - activeTitle: component.activeTitle, - activeValue: component.activeValue, - activeTitleColor: component.activeTextColor, - badgeIconName: "", - badgeText: nil, - badgePosition: 0.0, - badgeGraphPosition: 0.0, - isPremiumDisabled: false - ), - availableSize: CGSize(width: context.availableSize.width - sideInset * 2.0, height: context.availableSize.height), - transition: .immediate - ) - - context.add(title - .position(CGPoint(x: textSideInset + title.size.width / 2.0, y: textTopInset + title.size.height / 2.0)) - ) - - context.add(text - .position(CGPoint(x: textSideInset + text.size.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height / 2.0)) - ) - - context.add(limit - .position(CGPoint(x: context.availableSize.width / 2.0, y: textTopInset + title.size.height + spacing + text.size.height - 20.0)) - ) - - return CGSize(width: context.availableSize.width, height: textTopInset + title.size.height + text.size.height + 56.0) - } - } -} - -private enum Limit: CaseIterable { - case groups - case pins - case publicLinks - case savedGifs - case favedStickers - case about - case captions - case folders - case chatsPerFolder - case account - - func title(strings: PresentationStrings) -> String { - switch self { - case .groups: - return strings.Premium_Limits_GroupsAndChannels - case .pins: - return strings.Premium_Limits_PinnedChats - case .publicLinks: - return strings.Premium_Limits_PublicLinks - case .savedGifs: - return strings.Premium_Limits_SavedGifs - case .favedStickers: - return strings.Premium_Limits_FavedStickers - case .about: - return strings.Premium_Limits_Bio - case .captions: - return strings.Premium_Limits_Captions - case .folders: - return strings.Premium_Limits_Folders - case .chatsPerFolder: - return strings.Premium_Limits_ChatsPerFolder - case .account: - return strings.Premium_Limits_Accounts - } - } - - func text(strings: PresentationStrings) -> String { - switch self { - case .groups: - return strings.Premium_Limits_GroupsAndChannelsInfo - case .pins: - return strings.Premium_Limits_PinnedChatsInfo - case .publicLinks: - return strings.Premium_Limits_PublicLinksInfo - case .savedGifs: - return strings.Premium_Limits_SavedGifsInfo - case .favedStickers: - return strings.Premium_Limits_FavedStickersInfo - case .about: - return strings.Premium_Limits_BioInfo - case .captions: - return strings.Premium_Limits_CaptionsInfo - case .folders: - return strings.Premium_Limits_FoldersInfo - case .chatsPerFolder: - return strings.Premium_Limits_ChatsPerFolderInfo - case .account: - return strings.Premium_Limits_AccountsInfo - } - } - - func limit(_ configuration: EngineConfiguration.UserLimits, isPremium: Bool) -> String { - let value: Int32 - switch self { - case .groups: - value = configuration.maxChannelsCount - case .pins: - value = configuration.maxPinnedChatCount - case .publicLinks: - value = configuration.maxPublicLinksCount - case .savedGifs: - value = configuration.maxSavedGifCount - case .favedStickers: - value = configuration.maxFavedStickerCount - case .about: - value = configuration.maxAboutLength - case .captions: - value = configuration.maxCaptionLength - case .folders: - value = configuration.maxFoldersCount - case .chatsPerFolder: - value = configuration.maxFolderChatsCount - case .account: - value = isPremium ? 4 : 3 - } - return "\(value)" - } -} - -private final class LimitsListComponent: CombinedComponent { - typealias EnvironmentType = (Empty, ScrollChildEnvironment) - - let context: AccountContext - let topInset: CGFloat - let bottomInset: CGFloat - - init(context: AccountContext, topInset: CGFloat, bottomInset: CGFloat) { - self.context = context - self.topInset = topInset - self.bottomInset = bottomInset - } - - static func ==(lhs: LimitsListComponent, rhs: LimitsListComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } - if lhs.topInset != rhs.topInset { - return false - } - if lhs.bottomInset != rhs.bottomInset { - return false - } - return true - } - - final class State: ComponentState { - private let context: AccountContext - - private var disposable: Disposable? - var limits: EngineConfiguration.UserLimits = .defaultValue - var premiumLimits: EngineConfiguration.UserLimits = .defaultValue - - init(context: AccountContext) { - self.context = context - - super.init() - - self.disposable = (context.engine.data.get( - TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: false), - TelegramEngine.EngineData.Item.Configuration.UserLimits(isPremium: true) - ) - |> deliverOnMainQueue).start(next: { [weak self] limits, premiumLimits in - if let strongSelf = self { - strongSelf.limits = limits - strongSelf.premiumLimits = premiumLimits - strongSelf.updated(transition: .immediate) - } - }) - } - - deinit { - self.disposable?.dispose() - } - } - - func makeState() -> State { - return State(context: self.context) - } - - static var body: Body { - let list = Child(List.self) - - return { context in - let state = context.state - let theme = context.component.context.sharedContext.currentPresentationData.with { $0 }.theme - let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings - - let colors = [ - UIColor(rgb: 0x5ba0ff), - UIColor(rgb: 0x798aff), - UIColor(rgb: 0x9377ff), - UIColor(rgb: 0xac64f3), - UIColor(rgb: 0xc456ae), - UIColor(rgb: 0xcf579a), - UIColor(rgb: 0xdb5887), - UIColor(rgb: 0xdb496f), - UIColor(rgb: 0xe95d44), - UIColor(rgb: 0xf2822a) - ] - - let items: [AnyComponentWithIdentity] = Limit.allCases.enumerated().map { index, value in - AnyComponentWithIdentity( - id: value, component: AnyComponent( - LimitComponent( - title: value.title(strings: strings), - titleColor: theme.list.itemPrimaryTextColor, - text: value.text(strings: strings), - textColor: theme.list.itemSecondaryTextColor, - accentColor: theme.list.itemAccentColor, - inactiveColor: theme.list.itemBlocksSeparatorColor.withAlphaComponent(0.5), - inactiveTextColor: theme.list.itemPrimaryTextColor, - inactiveTitle: strings.Premium_Free, - inactiveValue: value.limit(state.limits, isPremium: false), - activeColor: colors[index], - activeTextColor: .white, - activeTitle: strings.Premium_Premium, - activeValue: value.limit(state.premiumLimits, isPremium: true) - ) - ) - ) - } - - let list = list.update( - component: List(items), - availableSize: CGSize(width: context.availableSize.width, height: 10000.0), - transition: context.transition - ) - - let contentHeight = context.component.topInset + list.size.height + context.component.bottomInset - context.add(list - .position(CGPoint(x: list.size.width / 2.0, y: context.component.topInset + list.size.height / 2.0)) - ) - - return CGSize(width: context.availableSize.width, height: contentHeight) - } - } -} - -private final class LimitsPageComponent: CombinedComponent { - typealias EnvironmentType = DemoPageEnvironment - - let context: AccountContext - let bottomInset: CGFloat - let updatedBottomAlpha: (CGFloat) -> Void - let updatedDismissOffset: (CGFloat) -> Void - let updatedIsDisplaying: (Bool) -> Void - - init(context: AccountContext, bottomInset: CGFloat, updatedBottomAlpha: @escaping (CGFloat) -> Void, updatedDismissOffset: @escaping (CGFloat) -> Void, updatedIsDisplaying: @escaping (Bool) -> Void) { - self.context = context - self.bottomInset = bottomInset - self.updatedBottomAlpha = updatedBottomAlpha - self.updatedDismissOffset = updatedDismissOffset - self.updatedIsDisplaying = updatedIsDisplaying - } - - static func ==(lhs: LimitsPageComponent, rhs: LimitsPageComponent) -> Bool { - if lhs.context !== rhs.context { - return false - } - if lhs.bottomInset != rhs.bottomInset { - return false - } - return true - } - - final class State: ComponentState { - let updateBottomAlpha: (CGFloat) -> Void - let updateDismissOffset: (CGFloat) -> Void - let updatedIsDisplaying: (Bool) -> Void - - var resetScroll: ActionSlot? - - var topContentOffset: CGFloat = 0.0 - var bottomContentOffset: CGFloat = 100.0 { - didSet { - self.updateAlpha() - } - } - - var position: CGFloat? { - didSet { - self.updateAlpha() - } - } - - var isDisplaying = false { - didSet { - if oldValue != self.isDisplaying { - self.updatedIsDisplaying(self.isDisplaying) - - if !self.isDisplaying { - self.resetScroll?.invoke(Void()) - } - } - } - } - - init(updateBottomAlpha: @escaping (CGFloat) -> Void, updateDismissOffset: @escaping (CGFloat) -> Void, updateIsDisplaying: @escaping (Bool) -> Void) { - self.updateBottomAlpha = updateBottomAlpha - self.updateDismissOffset = updateDismissOffset - self.updatedIsDisplaying = updateIsDisplaying - - super.init() - } - - func updateAlpha() { - let dismissPosition = min(1.0, abs(self.position ?? 0.0) / 1.3333) - let position = min(1.0, abs(self.position ?? 0.0)) - self.updateDismissOffset(dismissPosition) - - let verticalPosition = 1.0 - min(30.0, self.bottomContentOffset) / 30.0 - - let backgroundAlpha: CGFloat = max(position, verticalPosition) - self.updateBottomAlpha(backgroundAlpha) - } - } - - func makeState() -> State { - return State(updateBottomAlpha: self.updatedBottomAlpha, updateDismissOffset: self.updatedDismissOffset, updateIsDisplaying: self.updatedIsDisplaying) - } - - static var body: Body { - let background = Child(Rectangle.self) - let scroll = Child(ScrollComponent.self) - let topPanel = Child(BlurredBackgroundComponent.self) - let topSeparator = Child(Rectangle.self) - let title = Child(MultilineTextComponent.self) - - let resetScroll = ActionSlot() - - return { context in - let state = context.state - - let environment = context.environment[DemoPageEnvironment.self].value - state.resetScroll = resetScroll - state.position = environment.position - state.isDisplaying = environment.isDisplaying - - let theme = context.component.context.sharedContext.currentPresentationData.with { $0 }.theme - let strings = context.component.context.sharedContext.currentPresentationData.with { $0 }.strings - - let topInset: CGFloat = 56.0 - - let scroll = scroll.update( - component: ScrollComponent( - content: AnyComponent( - LimitsListComponent( - context: context.component.context, - topInset: topInset, - bottomInset: context.component.bottomInset - ) - ), - contentInsets: UIEdgeInsets(top: topInset, left: 0.0, bottom: 0.0, right: 0.0), - contentOffsetUpdated: { [weak state] topContentOffset, bottomContentOffset in - state?.topContentOffset = topContentOffset - state?.bottomContentOffset = bottomContentOffset - Queue.mainQueue().justDispatch { - state?.updated(transition: .immediate) - } - }, - contentOffsetWillCommit: { _ in }, - resetScroll: resetScroll - ), - availableSize: context.availableSize, - transition: context.transition - ) - - let background = background.update( - component: Rectangle(color: theme.list.plainBackgroundColor), - availableSize: scroll.size, - transition: context.transition - ) - context.add(background - .position(CGPoint(x: context.availableSize.width / 2.0, y: background.size.height / 2.0)) - ) - - context.add(scroll - .position(CGPoint(x: context.availableSize.width / 2.0, y: scroll.size.height / 2.0)) - ) - - let topPanel = topPanel.update( - component: BlurredBackgroundComponent( - color: theme.rootController.navigationBar.blurredBackgroundColor - ), - availableSize: CGSize(width: context.availableSize.width, height: topInset), - transition: context.transition - ) - - let topSeparator = topSeparator.update( - component: Rectangle( - color: theme.rootController.navigationBar.separatorColor - ), - availableSize: CGSize(width: context.availableSize.width, height: UIScreenPixel), - transition: context.transition - ) - - let title = title.update( - component: MultilineTextComponent( - text: .plain(NSAttributedString(string: strings.Premium_DoubledLimits, font: Font.semibold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)), - horizontalAlignment: .center, - truncationType: .end, - maximumNumberOfLines: 1 - ), - availableSize: context.availableSize, - transition: context.transition - ) - - let topPanelAlpha: CGFloat = min(30.0, state.topContentOffset) / 30.0 - context.add(topPanel - .position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0)) - .opacity(topPanelAlpha) - ) - context.add(topSeparator - .position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height)) - .opacity(topPanelAlpha) - ) - context.add(title - .position(CGPoint(x: context.availableSize.width / 2.0, y: topPanel.size.height / 2.0)) - ) - - return scroll.size - } - } -} - public class PremiumLimitsListScreen: ViewController { final class Node: ViewControllerTracingNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { private var presentationData: PresentationData diff --git a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift index 37b9aa36ad..f7590d6e0e 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/PendingMessageUploadedContent.swift @@ -53,10 +53,10 @@ enum MessageContentToUpload { } func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, forceReupload: Bool, isGrouped: Bool, message: Message) -> MessageContentToUpload { - return messageContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: false, peerId: message.id.peerId, messageId: message.id, attributes: message.attributes, text: message.text, media: message.media) + return messageContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: false, forceNoBigParts: false, peerId: message.id.peerId, messageId: message.id, attributes: message.attributes, text: message.text, media: message.media) } -func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, forceReupload: Bool, isGrouped: Bool, passFetchProgress: Bool, peerId: PeerId, messageId: MessageId?, attributes: [MessageAttribute], text: String, media: [Media]) -> MessageContentToUpload { +func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, forceReupload: Bool, isGrouped: Bool, passFetchProgress: Bool, forceNoBigParts: Bool, peerId: PeerId, messageId: MessageId?, attributes: [MessageAttribute], text: String, media: [Media]) -> MessageContentToUpload { var contextResult: OutgoingChatContextResultMessageAttribute? var autoremoveMessageAttribute: AutoremoveTimeoutMessageAttribute? var autoclearMessageAttribute: AutoclearTimeoutMessageAttribute? @@ -96,14 +96,14 @@ func messageContentToUpload(accountPeerId: PeerId, network: Network, postbox: Po return .content(PendingMessageUploadedContentAndReuploadInfo(content: .media(.inputMediaStory(userId: inputUser, id: media.storyId.id), ""), reuploadInfo: nil, cacheReferenceKey: nil)) } |> castError(PendingMessageUploadError.self), .text) - } else if let media = media.first, let mediaResult = mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: passFetchProgress, peerId: peerId, media: media, text: text, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, messageId: messageId, attributes: attributes) { + } else if let media = media.first, let mediaResult = mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: passFetchProgress, forceNoBigParts: forceNoBigParts, peerId: peerId, media: media, text: text, autoremoveMessageAttribute: autoremoveMessageAttribute, autoclearMessageAttribute: autoclearMessageAttribute, messageId: messageId, attributes: attributes) { return .signal(mediaResult, .media) } else { return .signal(.single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .text(text), reuploadInfo: nil, cacheReferenceKey: nil))), .text) } } -func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, forceReupload: Bool, isGrouped: Bool, passFetchProgress: Bool, peerId: PeerId, media: Media, text: String, autoremoveMessageAttribute: AutoremoveTimeoutMessageAttribute?, autoclearMessageAttribute: AutoclearTimeoutMessageAttribute?, messageId: MessageId?, attributes: [MessageAttribute]) -> Signal? { +func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, forceReupload: Bool, isGrouped: Bool, passFetchProgress: Bool, forceNoBigParts: Bool, peerId: PeerId, media: Media, text: String, autoremoveMessageAttribute: AutoremoveTimeoutMessageAttribute?, autoclearMessageAttribute: AutoclearTimeoutMessageAttribute?, messageId: MessageId?, attributes: [MessageAttribute]) -> Signal? { if let image = media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { if peerId.namespace == Namespaces.Peer.SecretChat, let resource = largest.resource as? SecretFileMediaResource { return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .secretMedia(.inputEncryptedFile(id: resource.fileId, accessHash: resource.accessHash), resource.decryptedSize, resource.key), reuploadInfo: nil, cacheReferenceKey: nil))) @@ -123,7 +123,7 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post } } } - return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: true, isGrouped: isGrouped, passFetchProgress: false, peerId: peerId, messageId: messageId, text: text, attributes: attributes, file: file) + return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: true, isGrouped: isGrouped, passFetchProgress: false, forceNoBigParts: false, peerId: peerId, messageId: messageId, text: text, attributes: attributes, file: file) } else { if forceReupload { let mediaReference: AnyMediaReference @@ -157,7 +157,7 @@ func mediaContentToUpload(accountPeerId: PeerId, network: Network, postbox: Post return .single(.content(PendingMessageUploadedContentAndReuploadInfo(content: .media(Api.InputMedia.inputMediaDocument(flags: flags, id: Api.InputDocument.inputDocument(id: resource.fileId, accessHash: resource.accessHash, fileReference: Buffer(data: resource.fileReference ?? Data())), ttlSeconds: nil, query: emojiSearchQuery), text), reuploadInfo: nil, cacheReferenceKey: nil))) } } else { - return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: passFetchProgress, peerId: peerId, messageId: messageId, text: text, attributes: attributes, file: file) + return uploadedMediaFileContent(network: network, postbox: postbox, auxiliaryMethods: auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, forceReupload: forceReupload, isGrouped: isGrouped, passFetchProgress: passFetchProgress, forceNoBigParts: forceNoBigParts, peerId: peerId, messageId: messageId, text: text, attributes: attributes, file: file) } } else if let contact = media as? TelegramMediaContact { let input = Api.InputMedia.inputMediaContact(phoneNumber: contact.phoneNumber, firstName: contact.firstName, lastName: contact.lastName, vcard: contact.vCardData ?? "") @@ -619,8 +619,8 @@ private enum UploadedMediaFileAndThumbnail { case done(TelegramMediaFile, UploadedMediaThumbnailResult) } -private func uploadedThumbnail(network: Network, postbox: Postbox, resourceReference: MediaResourceReference) -> Signal { - return multipartUpload(network: network, postbox: postbox, source: .resource(resourceReference), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: false) +private func uploadedThumbnail(network: Network, postbox: Postbox, resourceReference: MediaResourceReference, forceNoBigParts: Bool = false) -> Signal { + return multipartUpload(network: network, postbox: postbox, source: .resource(resourceReference), encrypt: false, tag: TelegramMediaResourceFetchTag(statsCategory: .image, userContentType: .image), hintFileSize: nil, hintFileIsLarge: false, forceNoBigParts: forceNoBigParts) |> mapError { _ -> PendingMessageUploadError in return .generic } |> mapToSignal { result -> Signal in switch result { @@ -656,7 +656,7 @@ public func statsCategoryForFileWithAttributes(_ attributes: [TelegramMediaFileA return .file } -private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, forceReupload: Bool, isGrouped: Bool, passFetchProgress: Bool, peerId: PeerId, messageId: MessageId?, text: String, attributes: [MessageAttribute], file: TelegramMediaFile) -> Signal { +private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxiliaryMethods: AccountAuxiliaryMethods, transformOutgoingMessageMedia: TransformOutgoingMessageMedia?, messageMediaPreuploadManager: MessageMediaPreuploadManager, forceReupload: Bool, isGrouped: Bool, passFetchProgress: Bool, forceNoBigParts: Bool, peerId: PeerId, messageId: MessageId?, text: String, attributes: [MessageAttribute], file: TelegramMediaFile) -> Signal { return maybePredownloadedFileResource(postbox: postbox, auxiliaryMethods: auxiliaryMethods, peerId: peerId, resource: file.resource, forceRefresh: forceReupload) |> mapToSignal { result -> Signal in var referenceKey: CachedSentMediaReferenceKey? @@ -781,7 +781,7 @@ private func uploadedMediaFileContent(network: Network, postbox: Postbox, auxili fileReference = .standalone(media: media) } - return uploadedThumbnail(network: network, postbox: postbox, resourceReference: fileReference.resourceReference(smallestThumbnail.resource)) + return uploadedThumbnail(network: network, postbox: postbox, resourceReference: fileReference.resourceReference(smallestThumbnail.resource), forceNoBigParts: forceNoBigParts) |> mapError { _ -> PendingMessageUploadError in return .generic } |> map { result in if let result = result { diff --git a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift index 3ddeca6417..3d3fd6be4a 100644 --- a/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift +++ b/submodules/TelegramCore/Sources/PendingMessages/RequestEditMessage.swift @@ -59,7 +59,7 @@ private func requestEditMessageInternal(accountPeerId: PeerId, postbox: Postbox, case let .update(media): let generateUploadSignal: (Bool) -> Signal? = { forceReupload in let augmentedMedia = augmentMediaWithReference(media) - return mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: stateManager.auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: mediaReferenceRevalidationContext, forceReupload: forceReupload, isGrouped: false, passFetchProgress: false, peerId: messageId.peerId, media: augmentedMedia, text: "", autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, messageId: nil, attributes: []) + return mediaContentToUpload(accountPeerId: accountPeerId, network: network, postbox: postbox, auxiliaryMethods: stateManager.auxiliaryMethods, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: mediaReferenceRevalidationContext, forceReupload: forceReupload, isGrouped: false, passFetchProgress: false, forceNoBigParts: false, peerId: messageId.peerId, media: augmentedMedia, text: "", autoremoveMessageAttribute: nil, autoclearMessageAttribute: nil, messageId: nil, attributes: []) } if let uploadSignal = generateUploadSignal(forceReupload) { uploadedMedia = .single(.progress(0.027)) diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift index b681db18d7..445370d041 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/PendingStoryManager.swift @@ -284,7 +284,6 @@ final class PendingStoryManager { self.currentPendingItemContext = pendingItemContext let stableId = firstItem.stableId - Logger.shared.log("PendingStoryManager", "setting up item context for: \(firstItem.stableId) randomId: \(firstItem.randomId)") pendingItemContext.disposable = (_internal_uploadStoryImpl(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, stateManager: self.stateManager, messageMediaPreuploadManager: self.messageMediaPreuploadManager, revalidationContext: self.revalidationContext, auxiliaryMethods: self.auxiliaryMethods, stableId: stableId, media: firstItem.media, text: firstItem.text, entities: firstItem.entities, embeddedStickers: firstItem.embeddedStickers, pin: firstItem.pin, privacy: firstItem.privacy, isForwardingDisabled: firstItem.isForwardingDisabled, period: Int(firstItem.period), randomId: firstItem.randomId) |> deliverOn(self.queue)).start(next: { [weak self] event in guard let `self` = self else { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift index 13c7c2fcbd..1825219a1a 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/Stories.swift @@ -688,6 +688,7 @@ private func uploadedStoryContent(postbox: Postbox, network: Network, media: Med forceReupload: true, isGrouped: false, passFetchProgress: passFetchProgress, + forceNoBigParts: true, peerId: accountPeerId, messageId: nil, attributes: attributes, @@ -782,7 +783,6 @@ func _internal_uploadStory(account: Account, media: EngineStoryInputMedia, text: period: Int32(period), randomId: randomId )) - Logger.shared.log("UploadStory", "Appended new pending item stableId: \(stableId) randomId: \(randomId)") transaction.setLocalStoryState(state: CodableEntry(currentState)) }).start() } @@ -826,7 +826,6 @@ private func _internal_putPendingStoryIdMapping(accountPeerId: PeerId, stableId: } func _internal_uploadStoryImpl(postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager, messageMediaPreuploadManager: MessageMediaPreuploadManager, revalidationContext: MediaReferenceRevalidationContext, auxiliaryMethods: AccountAuxiliaryMethods, stableId: Int32, media: Media, text: String, entities: [MessageTextEntity], embeddedStickers: [TelegramMediaFile], pin: Bool, privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, period: Int, randomId: Int64) -> Signal { - Logger.shared.log("UploadStory", "uploadStoryImpl for stableId: \(stableId) randomId: \(randomId)") let passFetchProgress = media is TelegramMediaFile let (contentSignal, originalMedia) = uploadedStoryContent(postbox: postbox, network: network, media: media, embeddedStickers: embeddedStickers, accountPeerId: accountPeerId, messageMediaPreuploadManager: messageMediaPreuploadManager, revalidationContext: revalidationContext, auxiliaryMethods: auxiliaryMethods, passFetchProgress: passFetchProgress) return contentSignal diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index e9c159fffb..ec86ca6fc2 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -1651,9 +1651,15 @@ public class CameraScreen: ViewController { guard let camera = self.camera else { return } - let location = gestureRecognizer.location(in: self.mainPreviewView) - let point = self.mainPreviewView.cameraPoint(for: location) - camera.focus(at: point, autoFocus: false) + + let location = gestureRecognizer.location(in: gestureRecognizer.view) + if self.cameraState.isDualCameraEnabled && self.additionalPreviewContainerView.frame.contains(location) { + self.toggleCameraPositionAction.invoke(Void()) + } else { + let location = gestureRecognizer.location(in: self.mainPreviewView) + let point = self.mainPreviewView.cameraPoint(for: location) + camera.focus(at: point, autoFocus: false) + } } @objc private func handleDoubleTap(_ gestureRecognizer: UITapGestureRecognizer) { @@ -2479,7 +2485,22 @@ public class CameraScreen: ViewController { transitionOut: transitionOut ) if let asset = result as? PHAsset { - self.completion(.single(.asset(asset)), resultTransition, dismissed) + if asset.mediaType == .video && asset.duration < 1.0 { + let presentationData = self.context.sharedContext.currentPresentationData.with { $0 } + let alertController = textAlertController( + context: self.context, + forceTheme: defaultDarkColorPresentationTheme, + title: nil, + text: presentationData.strings.Story_Editor_VideoTooShort, + actions: [ + TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}) + ], + actionLayout: .vertical + ) + self.present(alertController, in: .window(.root)) + } else { + self.completion(.single(.asset(asset)), resultTransition, dismissed) + } } else if let draft = result as? MediaEditorDraft { self.completion(.single(.draft(draft)), resultTransition, dismissed) } diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorAdjustments.metal b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorAdjustments.metal index abd02e5a0f..31f4f02380 100644 --- a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorAdjustments.metal +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorAdjustments.metal @@ -21,7 +21,7 @@ typedef struct { float grain; float vignette; float hasCurves; - float2 empty; + float2 empty; } MediaEditorAdjustments; half3 fade(half3 color, float fadeAmount) { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index c33886bb00..3eadbfe9f8 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -805,154 +805,3 @@ public final class MediaEditor { } } } - -final class MediaEditorRenderChain { - fileprivate let enhancePass = EnhanceRenderPass() - fileprivate let sharpenPass = SharpenRenderPass() - fileprivate let blurPass = BlurRenderPass() - fileprivate let adjustmentsPass = AdjustmentsRenderPass() - - var renderPasses: [RenderPass] { - return [ - self.enhancePass, - self.sharpenPass, - self.blurPass, - self.adjustmentsPass - ] - } - - func update(values: MediaEditorValues) { - for key in EditorToolKey.allCases { - let value = values.toolValues[key] - switch key { - case .enhance: - if let value = value as? Float { - self.enhancePass.value = abs(value) - } else { - self.enhancePass.value = 0.0 - } - case .brightness: - if let value = value as? Float { - self.adjustmentsPass.adjustments.exposure = value - } else { - self.adjustmentsPass.adjustments.exposure = 0.0 - } - case .contrast: - if let value = value as? Float { - self.adjustmentsPass.adjustments.contrast = value - } else { - self.adjustmentsPass.adjustments.contrast = 0.0 - } - case .saturation: - if let value = value as? Float { - self.adjustmentsPass.adjustments.saturation = value - } else { - self.adjustmentsPass.adjustments.saturation = 0.0 - } - case .warmth: - if let value = value as? Float { - self.adjustmentsPass.adjustments.warmth = value - } else { - self.adjustmentsPass.adjustments.warmth = 0.0 - } - case .fade: - if let value = value as? Float { - self.adjustmentsPass.adjustments.fade = value - } else { - self.adjustmentsPass.adjustments.fade = 0.0 - } - case .highlights: - if let value = value as? Float { - self.adjustmentsPass.adjustments.highlights = value - } else { - self.adjustmentsPass.adjustments.highlights = 0.0 - } - case .shadows: - if let value = value as? Float { - self.adjustmentsPass.adjustments.shadows = value - } else { - self.adjustmentsPass.adjustments.shadows = 0.0 - } - case .vignette: - if let value = value as? Float { - self.adjustmentsPass.adjustments.vignette = value - } else { - self.adjustmentsPass.adjustments.vignette = 0.0 - } - case .grain: - if let value = value as? Float { - self.adjustmentsPass.adjustments.grain = value - } else { - self.adjustmentsPass.adjustments.grain = 0.0 - } - case .sharpen: - if let value = value as? Float { - self.sharpenPass.value = value - } else { - self.sharpenPass.value = 0.0 - } - case .shadowsTint: - if let value = value as? TintValue { - if value.color != .clear { - let (red, green, blue, _) = value.color.components - self.adjustmentsPass.adjustments.shadowsTintColor = simd_float3(Float(red), Float(green), Float(blue)) - self.adjustmentsPass.adjustments.shadowsTintIntensity = value.intensity - } else { - self.adjustmentsPass.adjustments.shadowsTintIntensity = 0.0 - } - } - case .highlightsTint: - if let value = value as? TintValue { - if value.color != .clear { - let (red, green, blue, _) = value.color.components - self.adjustmentsPass.adjustments.shadowsTintColor = simd_float3(Float(red), Float(green), Float(blue)) - self.adjustmentsPass.adjustments.highlightsTintIntensity = value.intensity - } else { - self.adjustmentsPass.adjustments.highlightsTintIntensity = 0.0 - } - } - case .blur: - if let value = value as? BlurValue { - switch value.mode { - case .off: - self.blurPass.mode = .off - case .linear: - self.blurPass.mode = .linear - case .radial: - self.blurPass.mode = .radial - case .portrait: - self.blurPass.mode = .portrait - } - self.blurPass.intensity = value.intensity - self.blurPass.value.size = Float(value.size) - self.blurPass.value.position = simd_float2(Float(value.position.x), Float(value.position.y)) - self.blurPass.value.falloff = Float(value.falloff) - self.blurPass.value.rotation = Float(value.rotation) - } - case .curves: - if var value = value as? CurvesValue { - let allDataPoints = value.all.dataPoints - let redDataPoints = value.red.dataPoints - let greenDataPoints = value.green.dataPoints - let blueDataPoints = value.blue.dataPoints - - self.adjustmentsPass.adjustments.hasCurves = 1.0 - self.adjustmentsPass.allCurve = allDataPoints - self.adjustmentsPass.redCurve = redDataPoints - self.adjustmentsPass.greenCurve = greenDataPoints - self.adjustmentsPass.blueCurve = blueDataPoints - } else { - self.adjustmentsPass.adjustments.hasCurves = 0.0 - } - } - } - } -} - -public func debugSaveImage(_ image: UIImage, name: String) { - let path = NSTemporaryDirectory() + "debug_\(name)_\(Int64.random(in: .min ... .max)).png" - print(path) - if let data = image.pngData() { - try? data.write(to: URL(fileURLWithPath: path)) - } -} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift index d837b355cc..a66568f0fb 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift @@ -42,6 +42,7 @@ final class MediaEditorComposer { private let values: MediaEditorValues private let dimensions: CGSize private let outputDimensions: CGSize + private let textScale: CGFloat private let ciContext: CIContext? private var textureCache: CVMetalTextureCache? @@ -53,10 +54,11 @@ final class MediaEditorComposer { private let drawingImage: CIImage? private var entities: [MediaEditorComposerEntity] - init(account: Account, values: MediaEditorValues, dimensions: CGSize, outputDimensions: CGSize) { + init(account: Account, values: MediaEditorValues, dimensions: CGSize, outputDimensions: CGSize, textScale: CGFloat) { self.values = values self.dimensions = dimensions self.outputDimensions = outputDimensions + self.textScale = textScale let colorSpace = CGColorSpaceCreateDeviceRGB() self.colorSpace = colorSpace @@ -77,7 +79,7 @@ final class MediaEditorComposer { var entities: [MediaEditorComposerEntity] = [] for entity in values.entities { - entities.append(contentsOf: composerEntitiesForDrawingEntity(account: account, entity: entity.entity, colorSpace: colorSpace)) + entities.append(contentsOf: composerEntitiesForDrawingEntity(account: account, textScale: textScale, entity: entity.entity, colorSpace: colorSpace)) } self.entities = entities @@ -115,7 +117,6 @@ final class MediaEditorComposer { var pixelBuffer: CVPixelBuffer? CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pool, &pixelBuffer) - if let pixelBuffer { processImage(inputImage: ciImage, time: time, completion: { compositedImage in if var compositedImage { @@ -156,7 +157,7 @@ final class MediaEditorComposer { CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pool, &pixelBuffer) if let pixelBuffer, let context = self.ciContext { - makeEditorImageFrameComposition(context: context, inputImage: image, gradientImage: self.gradientImage, drawingImage: self.drawingImage, dimensions: self.dimensions, values: self.values, entities: self.entities, time: time, completion: { compositedImage in + makeEditorImageFrameComposition(context: context, inputImage: image, gradientImage: self.gradientImage, drawingImage: self.drawingImage, dimensions: self.dimensions, outputDimensions: self.outputDimensions, values: self.values, entities: self.entities, time: time, completion: { compositedImage in if var compositedImage { let scale = self.outputDimensions.width / self.dimensions.width compositedImage = compositedImage.samplingLinear().transformed(by: CGAffineTransform(scaleX: scale, y: scale)) @@ -177,11 +178,11 @@ final class MediaEditorComposer { guard let context = self.ciContext else { return } - makeEditorImageFrameComposition(context: context, inputImage: inputImage, gradientImage: self.gradientImage, drawingImage: self.drawingImage, dimensions: self.dimensions, values: self.values, entities: self.entities, time: time, completion: completion) + makeEditorImageFrameComposition(context: context, inputImage: inputImage, gradientImage: self.gradientImage, drawingImage: self.drawingImage, dimensions: self.dimensions, outputDimensions: self.outputDimensions, values: self.values, entities: self.entities, time: time, textScale: self.textScale, completion: completion) } } -public func makeEditorImageComposition(context: CIContext, account: Account, inputImage: UIImage, dimensions: CGSize, values: MediaEditorValues, time: CMTime, completion: @escaping (UIImage?) -> Void) { +public func makeEditorImageComposition(context: CIContext, account: Account, inputImage: UIImage, dimensions: CGSize, values: MediaEditorValues, time: CMTime, textScale: CGFloat, completion: @escaping (UIImage?) -> Void) { let colorSpace = CGColorSpaceCreateDeviceRGB() let inputImage = CIImage(image: inputImage, options: [.colorSpace: colorSpace])! let gradientImage: CIImage @@ -198,10 +199,10 @@ public func makeEditorImageComposition(context: CIContext, account: Account, inp var entities: [MediaEditorComposerEntity] = [] for entity in values.entities { - entities.append(contentsOf: composerEntitiesForDrawingEntity(account: account, entity: entity.entity, colorSpace: colorSpace)) + entities.append(contentsOf: composerEntitiesForDrawingEntity(account: account, textScale: textScale, entity: entity.entity, colorSpace: colorSpace)) } - makeEditorImageFrameComposition(context: context, inputImage: inputImage, gradientImage: gradientImage, drawingImage: drawingImage, dimensions: dimensions, values: values, entities: entities, time: time, completion: { ciImage in + makeEditorImageFrameComposition(context: context, inputImage: inputImage, gradientImage: gradientImage, drawingImage: drawingImage, dimensions: dimensions, outputDimensions: dimensions, values: values, entities: entities, time: time, textScale: textScale, completion: { ciImage in if let ciImage { if let cgImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: ciImage.extent.size)) { Queue.mainQueue().async { @@ -214,7 +215,7 @@ public func makeEditorImageComposition(context: CIContext, account: Account, inp }) } -private func makeEditorImageFrameComposition(context: CIContext, inputImage: CIImage, gradientImage: CIImage, drawingImage: CIImage?, dimensions: CGSize, values: MediaEditorValues, entities: [MediaEditorComposerEntity], time: CMTime, completion: @escaping (CIImage?) -> Void) { +private func makeEditorImageFrameComposition(context: CIContext, inputImage: CIImage, gradientImage: CIImage, drawingImage: CIImage?, dimensions: CGSize, outputDimensions: CGSize, values: MediaEditorValues, entities: [MediaEditorComposerEntity], time: CMTime, textScale: CGFloat = 1.0, completion: @escaping (CIImage?) -> Void) { var resultImage = CIImage(color: .black).cropped(to: CGRect(origin: .zero, size: dimensions)).transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) resultImage = gradientImage.composited(over: resultImage) @@ -268,8 +269,10 @@ private func makeEditorImageFrameComposition(context: CIContext, inputImage: CII image = image.transformed(by: resetTransform) var baseScale: CGFloat = 1.0 - if let entityBaseScale = entity.baseScale { - baseScale = entityBaseScale + if let scale = entity.baseScale { + baseScale = scale + } else if let _ = entity.baseDrawingSize { +// baseScale = textScale } else if let baseSize = entity.baseSize { baseScale = baseSize.width / image.extent.width } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift index 78acc111cc..c394b77b3d 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift @@ -12,11 +12,11 @@ import TelegramAnimatedStickerNode import YuvConversion import StickerResources -private func prerenderTextTransformations(entity: DrawingTextEntity, image: UIImage, colorSpace: CGColorSpace) -> MediaEditorComposerStaticEntity { +private func prerenderTextTransformations(entity: DrawingTextEntity, image: UIImage, textScale: CGFloat, colorSpace: CGColorSpace) -> MediaEditorComposerStaticEntity { let imageSize = image.size let angle = -entity.rotation - let scale = entity.scale + let scale = entity.scale * 0.5 * textScale let rotatedSize = CGSize( width: abs(imageSize.width * cos(angle)) + abs(imageSize.height * sin(angle)), @@ -41,12 +41,12 @@ private func prerenderTextTransformations(entity: DrawingTextEntity, image: UIIm if let cgImage = image.cgImage { context.draw(cgImage, in: drawRect) } - })! + }, scale: 1.0)! - return MediaEditorComposerStaticEntity(image: CIImage(image: newImage, options: [.colorSpace: colorSpace])!, position: entity.position, scale: 1.0, rotation: 0.0, baseSize: nil, baseScale: 0.333, mirrored: false) + return MediaEditorComposerStaticEntity(image: CIImage(image: newImage, options: [.colorSpace: colorSpace])!, position: entity.position, scale: 1.0, rotation: 0.0, baseSize: nil, baseDrawingSize: CGSize(width: 1080, height: 1920), mirrored: false) } -func composerEntitiesForDrawingEntity(account: Account, entity: DrawingEntity, colorSpace: CGColorSpace, tintColor: UIColor? = nil) -> [MediaEditorComposerEntity] { +func composerEntitiesForDrawingEntity(account: Account, textScale: CGFloat, entity: DrawingEntity, colorSpace: CGColorSpace, tintColor: UIColor? = nil) -> [MediaEditorComposerEntity] { if let entity = entity as? DrawingStickerEntity { let content: MediaEditorComposerStickerEntity.Content switch entity.content { @@ -62,19 +62,18 @@ func composerEntitiesForDrawingEntity(account: Account, entity: DrawingEntity, c return [MediaEditorComposerStickerEntity(account: account, content: content, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: entity.mirrored, colorSpace: colorSpace, tintColor: tintColor, isStatic: entity.isExplicitlyStatic)] } else if let renderImage = entity.renderImage, let image = CIImage(image: renderImage, options: [.colorSpace: colorSpace]) { if let entity = entity as? DrawingBubbleEntity { - return [MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: 1.0, rotation: entity.rotation, baseSize: entity.size, baseScale: nil, mirrored: false)] + return [MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: 1.0, rotation: entity.rotation, baseSize: entity.size, mirrored: false)] } else if let entity = entity as? DrawingSimpleShapeEntity { - return [MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: 1.0, rotation: entity.rotation, baseSize: entity.size, baseScale: nil, mirrored: false)] + return [MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: 1.0, rotation: entity.rotation, baseSize: entity.size, mirrored: false)] } else if let entity = entity as? DrawingVectorEntity { - return [MediaEditorComposerStaticEntity(image: image, position: CGPoint(x: entity.drawingSize.width * 0.5, y: entity.drawingSize.height * 0.5), scale: 1.0, rotation: 0.0, baseSize: entity.drawingSize, baseScale: nil, mirrored: false)] + return [MediaEditorComposerStaticEntity(image: image, position: CGPoint(x: entity.drawingSize.width * 0.5, y: entity.drawingSize.height * 0.5), scale: 1.0, rotation: 0.0, baseSize: entity.drawingSize, mirrored: false)] } else if let entity = entity as? DrawingTextEntity { var entities: [MediaEditorComposerEntity] = [] -// entities.append(prerenderTextTransformations(entity: entity, image: renderImage, colorSpace: colorSpace)) + entities.append(prerenderTextTransformations(entity: entity, image: renderImage, textScale: textScale, colorSpace: colorSpace)) - entities.append(MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: nil, baseScale: 0.5, mirrored: false)) if let renderSubEntities = entity.renderSubEntities { for subEntity in renderSubEntities { - entities.append(contentsOf: composerEntitiesForDrawingEntity(account: account, entity: subEntity, colorSpace: colorSpace, tintColor: entity.color.toUIColor())) + entities.append(contentsOf: composerEntitiesForDrawingEntity(account: account, textScale: textScale, entity: subEntity, colorSpace: colorSpace, tintColor: entity.color.toUIColor())) } } return entities @@ -90,15 +89,26 @@ private class MediaEditorComposerStaticEntity: MediaEditorComposerEntity { let rotation: CGFloat let baseSize: CGSize? let baseScale: CGFloat? + let baseDrawingSize: CGSize? let mirrored: Bool - init(image: CIImage, position: CGPoint, scale: CGFloat, rotation: CGFloat, baseSize: CGSize?, baseScale: CGFloat?, mirrored: Bool) { + init( + image: CIImage, + position: CGPoint, + scale: CGFloat, + rotation: CGFloat, + baseSize: CGSize?, + baseScale: CGFloat? = nil, + baseDrawingSize: CGSize? = nil, + mirrored: Bool + ) { self.image = image self.position = position self.scale = scale self.rotation = rotation self.baseSize = baseSize self.baseScale = baseScale + self.baseDrawingSize = baseDrawingSize self.mirrored = mirrored } @@ -127,6 +137,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { let rotation: CGFloat let baseSize: CGSize? let baseScale: CGFloat? = nil + let baseDrawingSize: CGSize? = nil let mirrored: Bool let colorSpace: CGColorSpace let tintColor: UIColor? @@ -475,6 +486,7 @@ protocol MediaEditorComposerEntity { var rotation: CGFloat { get } var baseSize: CGSize? { get } var baseScale: CGFloat? { get } + var baseDrawingSize: CGSize? { get } var mirrored: Bool { get } func image(for time: CMTime, frameRate: Float, context: CIContext, completion: @escaping (CIImage?) -> Void) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderChain.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderChain.swift new file mode 100644 index 0000000000..ad228ca79d --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderChain.swift @@ -0,0 +1,145 @@ +import Foundation +import simd + +final class MediaEditorRenderChain { + let enhancePass = EnhanceRenderPass() + let sharpenPass = SharpenRenderPass() + let blurPass = BlurRenderPass() + let adjustmentsPass = AdjustmentsRenderPass() + + var renderPasses: [RenderPass] { + return [ + self.enhancePass, + self.sharpenPass, + self.blurPass, + self.adjustmentsPass + ] + } + + func update(values: MediaEditorValues) { + for key in EditorToolKey.allCases { + let value = values.toolValues[key] + switch key { + case .enhance: + if let value = value as? Float { + self.enhancePass.value = abs(value) + } else { + self.enhancePass.value = 0.0 + } + case .brightness: + if let value = value as? Float { + self.adjustmentsPass.adjustments.exposure = value + } else { + self.adjustmentsPass.adjustments.exposure = 0.0 + } + case .contrast: + if let value = value as? Float { + self.adjustmentsPass.adjustments.contrast = value + } else { + self.adjustmentsPass.adjustments.contrast = 0.0 + } + case .saturation: + if let value = value as? Float { + self.adjustmentsPass.adjustments.saturation = value + } else { + self.adjustmentsPass.adjustments.saturation = 0.0 + } + case .warmth: + if let value = value as? Float { + self.adjustmentsPass.adjustments.warmth = value + } else { + self.adjustmentsPass.adjustments.warmth = 0.0 + } + case .fade: + if let value = value as? Float { + self.adjustmentsPass.adjustments.fade = value + } else { + self.adjustmentsPass.adjustments.fade = 0.0 + } + case .highlights: + if let value = value as? Float { + self.adjustmentsPass.adjustments.highlights = value + } else { + self.adjustmentsPass.adjustments.highlights = 0.0 + } + case .shadows: + if let value = value as? Float { + self.adjustmentsPass.adjustments.shadows = value + } else { + self.adjustmentsPass.adjustments.shadows = 0.0 + } + case .vignette: + if let value = value as? Float { + self.adjustmentsPass.adjustments.vignette = value + } else { + self.adjustmentsPass.adjustments.vignette = 0.0 + } + case .grain: + if let value = value as? Float { + self.adjustmentsPass.adjustments.grain = value + } else { + self.adjustmentsPass.adjustments.grain = 0.0 + } + case .sharpen: + if let value = value as? Float { + self.sharpenPass.value = value + } else { + self.sharpenPass.value = 0.0 + } + case .shadowsTint: + if let value = value as? TintValue { + if value.color != .clear { + let (red, green, blue, _) = value.color.components + self.adjustmentsPass.adjustments.shadowsTintColor = simd_float3(Float(red), Float(green), Float(blue)) + self.adjustmentsPass.adjustments.shadowsTintIntensity = value.intensity + } else { + self.adjustmentsPass.adjustments.shadowsTintIntensity = 0.0 + } + } + case .highlightsTint: + if let value = value as? TintValue { + if value.color != .clear { + let (red, green, blue, _) = value.color.components + self.adjustmentsPass.adjustments.shadowsTintColor = simd_float3(Float(red), Float(green), Float(blue)) + self.adjustmentsPass.adjustments.highlightsTintIntensity = value.intensity + } else { + self.adjustmentsPass.adjustments.highlightsTintIntensity = 0.0 + } + } + case .blur: + if let value = value as? BlurValue { + switch value.mode { + case .off: + self.blurPass.mode = .off + case .linear: + self.blurPass.mode = .linear + case .radial: + self.blurPass.mode = .radial + case .portrait: + self.blurPass.mode = .portrait + } + self.blurPass.intensity = value.intensity + self.blurPass.value.size = Float(value.size) + self.blurPass.value.position = simd_float2(Float(value.position.x), Float(value.position.y)) + self.blurPass.value.falloff = Float(value.falloff) + self.blurPass.value.rotation = Float(value.rotation) + } + case .curves: + if var value = value as? CurvesValue { + let allDataPoints = value.all.dataPoints + let redDataPoints = value.red.dataPoints + let greenDataPoints = value.green.dataPoints + let blueDataPoints = value.blue.dataPoints + + self.adjustmentsPass.adjustments.hasCurves = 1.0 + self.adjustmentsPass.allCurve = allDataPoints + self.adjustmentsPass.redCurve = redDataPoints + self.adjustmentsPass.greenCurve = greenDataPoints + self.adjustmentsPass.blueCurve = blueDataPoints + } else { + self.adjustmentsPass.adjustments.hasCurves = 0.0 + } + } + } + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift index 80324382c1..b1123a0ebc 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorVideoExport.swift @@ -261,6 +261,7 @@ public final class MediaEditorVideoExport { private let account: Account private let subject: Subject private let configuration: Configuration + private let textScale: CGFloat private let outputPath: String private var reader: AVAssetReader? @@ -295,11 +296,12 @@ public final class MediaEditorVideoExport { private let semaphore = DispatchSemaphore(value: 0) - public init(account: Account, subject: Subject, configuration: Configuration, outputPath: String) { + public init(account: Account, subject: Subject, configuration: Configuration, outputPath: String, textScale: CGFloat = 1.0) { self.account = account self.subject = subject self.configuration = configuration self.outputPath = outputPath + self.textScale = textScale if FileManager.default.fileExists(atPath: outputPath) { try? FileManager.default.removeItem(atPath: outputPath) @@ -354,7 +356,7 @@ public final class MediaEditorVideoExport { guard self.composer == nil else { return } - self.composer = MediaEditorComposer(account: self.account, values: self.configuration.values, dimensions: self.configuration.composerDimensions, outputDimensions: self.configuration.dimensions) + self.composer = MediaEditorComposer(account: self.account, values: self.configuration.values, dimensions: self.configuration.composerDimensions, outputDimensions: self.configuration.dimensions, textScale: self.textScale) } private func setupWithAsset(_ asset: AVAsset, additionalAsset: AVAsset?) { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index 0edd96f18f..d4f5aae5b4 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -35,8 +35,8 @@ swift_library( "//submodules/TelegramUI/Components/TextFieldComponent", "//submodules/TelegramUI/Components/ChatInputNode", "//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode", + "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TooltipUI", - "//submodules/Components/BlurredBackgroundComponent", "//submodules/AvatarNode", "//submodules/TelegramUI/Components/ShareWithPeersScreen", "//submodules/TelegramUI/Components/CameraButtonComponent", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 577cf65b5a..8f92f92060 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -19,7 +19,7 @@ import MessageInputPanelComponent import TextFieldComponent import EntityKeyboard import TooltipUI -import BlurredBackgroundComponent +import PlainButtonComponent import AvatarNode import ShareWithPeersScreen import PresentationDataUtils @@ -38,7 +38,6 @@ enum DrawingScreenType { case sticker } -private let privacyButtonTag = GenericComponentViewTag() private let muteButtonTag = GenericComponentViewTag() private let saveButtonTag = GenericComponentViewTag() @@ -237,11 +236,9 @@ final class MediaEditorScreenComponent: Component { private let scrubber = ComponentView() - private let privacyButton = ComponentView() private let flipStickerButton = ComponentView() private let muteButton = ComponentView() private let saveButton = ComponentView() - private let settingsButton = ComponentView() private let textCancelButton = ComponentView() private let textDoneButton = ComponentView() @@ -469,16 +466,6 @@ final class MediaEditorScreenComponent: Component { view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) } - if let view = self.settingsButton.view { - view.layer.animateAlpha(from: 0.0, to: view.alpha, duration: 0.2) - view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) - } - - if let view = self.privacyButton.view { - view.layer.animateAlpha(from: 0.0, to: view.alpha, duration: 0.2) - view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) - } - if let view = self.inputPanel.view { view.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) @@ -546,16 +533,6 @@ final class MediaEditorScreenComponent: Component { transition.setScale(view: view, scale: 0.1) } - if let view = self.settingsButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) - } - - if let view = self.privacyButton.view { - transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) - } - if let view = self.scrubber.view { view.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 44.0), duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true) view.layer.animateAlpha(from: view.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false) @@ -673,12 +650,7 @@ final class MediaEditorScreenComponent: Component { self.setupIfNeeded() - let isTablet: Bool - if case .regular = environment.metrics.widthClass { - isTablet = true - } else { - isTablet = false - } + let isTablet = environment.metrics.isTablet let openDrawing = component.openDrawing let openTools = component.openTools @@ -745,11 +717,12 @@ final class MediaEditorScreenComponent: Component { let doneButtonSize = self.doneButton.update( transition: transition, - component: AnyComponent(Button( - content: AnyComponent(DoneButtonComponent( + component: AnyComponent(PlainButtonComponent( + content: AnyComponent(DoneButtonContentComponent( backgroundColor: UIColor(rgb: 0x007aff), icon: UIImage(bundleImageName: "Media Editor/Next")!, title: doneButtonTitle.uppercased())), + effectAlignment: .center, action: { guard let controller = environment.controller() as? MediaEditorScreen else { return @@ -1282,70 +1255,9 @@ final class MediaEditorScreenComponent: Component { transition.setAlpha(view: inputPanelView, alpha: isEditingTextEntity || component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) } - let additionalPeersCount = component.privacy.privacy.additionallyIncludePeers.count - var privacyText: String - switch component.privacy.privacy.base { - case .everyone: - privacyText = environment.strings.Story_ContextPrivacy_LabelEveryone - case .closeFriends: - privacyText = environment.strings.Story_ContextPrivacy_LabelCloseFriends - case .contacts: - if additionalPeersCount > 0 { - privacyText = environment.strings.Story_ContextPrivacy_LabelContactsExcept("\(additionalPeersCount)").string - } else { - privacyText = environment.strings.Story_ContextPrivacy_LabelContacts - } - case .nobody: - if additionalPeersCount > 0 { - privacyText = environment.strings.Story_ContextPrivacy_LabelOnlySelected(Int32(additionalPeersCount)) - } else { - privacyText = environment.strings.Story_ContextPrivacy_LabelOnlyMe - } - } let displayTopButtons = !(self.inputPanelExternalState.isEditing || isEditingTextEntity || component.isDisplayingTool) - - let privacyButtonSize = self.privacyButton.update( - transition: transition, - component: AnyComponent(Button( - content: AnyComponent( - PrivacyButtonComponent( - backgroundColor: isTablet ? UIColor(rgb: 0x303030, alpha: 0.5) : UIColor(white: 0.0, alpha: 0.5), - icon: UIImage(bundleImageName: "Media Editor/Recipient")!, - text: privacyText - ) - ), - action: { - if let controller = environment.controller() as? MediaEditorScreen { - controller.openPrivacySettings() - } - } - ).tagged(privacyButtonTag)), - environment: {}, - containerSize: CGSize(width: 44.0, height: 44.0) - ) - let privacyButtonFrame: CGRect - if isTablet { - privacyButtonFrame = CGRect( - origin: CGPoint(x: availableSize.width - buttonSideInset - doneButtonSize.width - privacyButtonSize.width - 24.0, y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0), - size: privacyButtonSize - ) - } else { - privacyButtonFrame = CGRect( - origin: CGPoint(x: 16.0, y: environment.safeInsets.top + 20.0), - size: privacyButtonSize - ) - } - if let privacyButtonView = self.privacyButton.view { - if privacyButtonView.superview == nil { - //self.addSubview(privacyButtonView) - } - transition.setPosition(view: privacyButtonView, position: privacyButtonFrame.center) - transition.setBounds(view: privacyButtonView, bounds: CGRect(origin: .zero, size: privacyButtonFrame.size)) - transition.setScale(view: privacyButtonView, scale: displayTopButtons ? 1.0 : 0.01) - transition.setAlpha(view: privacyButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0) - } - + let saveContentComponent: AnyComponentWithIdentity if component.hasAppeared { saveContentComponent = AnyComponentWithIdentity( @@ -2747,10 +2659,8 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let context = context { let path = UIBezierPath(roundedRect: rect, cornerRadius: cornerRadius) - context.addPath(path.cgPath) context.clip() - image.draw(in: rect) } @@ -3410,7 +3320,13 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } private func openEditCategory(privacy: EngineStoryPrivacy, isForwardingDisabled: Bool, pin: Bool, completion: @escaping (EngineStoryPrivacy) -> Void) { - let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .contacts(privacy.base), initialPeerIds: Set(privacy.additionallyIncludePeers)) + let subject: ShareWithPeersScreen.StateContext.Subject + if privacy.base == .nobody { + subject = .chats + } else { + subject = .contacts(privacy.base) + } + let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: subject, initialPeerIds: Set(privacy.additionallyIncludePeers)) let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let self else { return @@ -3651,7 +3567,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let resultImage = mediaEditor.resultImage { mediaEditor.seek(0.0, andPlay: false) - makeEditorImageComposition(context: self.node.ciContext, account: self.context.account, inputImage: resultImage, dimensions: storyDimensions, values: values, time: .zero, completion: { resultImage in + makeEditorImageComposition(context: self.node.ciContext, account: self.context.account, inputImage: resultImage, dimensions: storyDimensions, values: values, time: .zero, textScale: 2.0, completion: { resultImage in guard let resultImage else { return } @@ -3813,9 +3729,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } else { duration = durationValue } - - let _ = additionalPath - + firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in let avAsset = AVURLAsset(url: URL(fileURLWithPath: path)) let avAssetGenerator = AVAssetImageGenerator(asset: avAsset) @@ -3936,7 +3850,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate inputImage = UIImage() } - makeEditorImageComposition(context: self.node.ciContext, account: self.context.account, inputImage: inputImage, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { [weak self] coverImage in + makeEditorImageComposition(context: self.node.ciContext, account: self.context.account, inputImage: inputImage, dimensions: storyDimensions, values: mediaEditor.values, time: firstFrameTime, textScale: 2.0, completion: { [weak self] coverImage in if let self { Logger.shared.log("MediaEditor", "Completed with video \(videoResult)") self.completion(randomId, .video(video: videoResult, coverImage: coverImage, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions), caption, self.state.privacy, stickers, { [weak self] finished in @@ -3959,7 +3873,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if let image = mediaEditor.resultImage { self.saveDraft(id: randomId) - makeEditorImageComposition(context: self.node.ciContext, account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { [weak self] resultImage in + makeEditorImageComposition(context: self.node.ciContext, account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, textScale: 2.0, completion: { [weak self] resultImage in if let self, let resultImage { Logger.shared.log("MediaEditor", "Completed with image \(resultImage)") self.completion(randomId, .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), caption, self.state.privacy, stickers, { [weak self] finished in @@ -4087,7 +4001,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } let configuration = recommendedVideoExportConfiguration(values: mediaEditor.values, duration: duration, forceFullHd: true, frameRate: 60.0) let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).mp4" - let videoExport = MediaEditorVideoExport(account: self.context.account, subject: exportSubject, configuration: configuration, outputPath: outputPath) + let videoExport = MediaEditorVideoExport(account: self.context.account, subject: exportSubject, configuration: configuration, outputPath: outputPath, textScale: 2.0) self.videoExport = videoExport videoExport.start() @@ -4120,7 +4034,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } else { if let image = mediaEditor.resultImage { Queue.concurrentDefaultQueue().async { - makeEditorImageComposition(context: self.node.ciContext, account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { resultImage in + makeEditorImageComposition(context: self.node.ciContext, account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, textScale: 2.0, completion: { resultImage in if let data = resultImage?.jpegData(compressionQuality: 0.8) { let outputPath = NSTemporaryDirectory() + "\(Int64.random(in: 0 ..< .max)).jpg" try? data.write(to: URL(fileURLWithPath: outputPath)) @@ -4135,10 +4049,6 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } - func requestSettings() { - - } - fileprivate func cancelVideoExport() { if let videoExport = self.videoExport { self.previousSavedValues = nil @@ -4213,80 +4123,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } } -final class PrivacyButtonComponent: CombinedComponent { - let backgroundColor: UIColor - let icon: UIImage - let text: String - - init( - backgroundColor: UIColor, - icon: UIImage, - text: String - ) { - self.backgroundColor = backgroundColor - self.icon = icon - self.text = text - } - - static func ==(lhs: PrivacyButtonComponent, rhs: PrivacyButtonComponent) -> Bool { - if lhs.backgroundColor != rhs.backgroundColor { - return false - } - if lhs.text != rhs.text { - return false - } - return true - } - - static var body: Body { - let background = Child(BlurredBackgroundComponent.self) - let icon = Child(Image.self) - let text = Child(Text.self) - - return { context in - let icon = icon.update( - component: Image(image: context.component.icon, size: CGSize(width: 9.0, height: 11.0)), - availableSize: CGSize(width: 180.0, height: 100.0), - transition: .immediate - ) - - let text = text.update( - component: Text( - text: context.component.text, - font: Font.medium(14.0), - color: .white - ), - availableSize: CGSize(width: 180.0, height: 100.0), - transition: .immediate - ) - - let backgroundSize = CGSize(width: text.size.width + 38.0, height: 30.0) - let background = background.update( - component: BlurredBackgroundComponent(color: context.component.backgroundColor), - availableSize: backgroundSize, - transition: .immediate - ) - - context.add(background - .position(CGPoint(x: backgroundSize.width / 2.0, y: backgroundSize.height / 2.0)) - .cornerRadius(min(backgroundSize.width, backgroundSize.height) / 2.0) - .clipsToBounds(true) - ) - - context.add(icon - .position(CGPoint(x: 16.0, y: backgroundSize.height / 2.0)) - ) - - context.add(text - .position(CGPoint(x: backgroundSize.width / 2.0 + 7.0, y: backgroundSize.height / 2.0)) - ) - - return backgroundSize - } - } -} - -final class DoneButtonComponent: CombinedComponent { +final class DoneButtonContentComponent: CombinedComponent { let backgroundColor: UIColor let icon: UIImage let title: String? @@ -4301,7 +4138,7 @@ final class DoneButtonComponent: CombinedComponent { self.title = title } - static func ==(lhs: DoneButtonComponent, rhs: DoneButtonComponent) -> Bool { + static func ==(lhs: DoneButtonContentComponent, rhs: DoneButtonContentComponent) -> Bool { if lhs.backgroundColor != rhs.backgroundColor { return false } diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift index 90511dd49b..7de762ec79 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/SaveProgressScreen.swift @@ -383,8 +383,6 @@ public final class SaveProgressScreenComponent: Component { } } -private let storyDimensions = CGSize(width: 1080.0, height: 1920.0) - public final class SaveProgressScreen: ViewController { fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { private weak var controller: SaveProgressScreen? diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/SectionHeaderComponent.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/SectionHeaderComponent.swift index 53589d0d8a..8d31a033d1 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/SectionHeaderComponent.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/SectionHeaderComponent.swift @@ -9,15 +9,21 @@ final class SectionHeaderComponent: Component { let theme: PresentationTheme let style: ShareWithPeersScreenComponent.Style let title: String + let actionTitle: String? + let action: (() -> Void)? init( theme: PresentationTheme, style: ShareWithPeersScreenComponent.Style, - title: String + title: String, + actionTitle: String?, + action: (() -> Void)? ) { self.theme = theme self.style = style self.title = title + self.actionTitle = actionTitle + self.action = action } static func ==(lhs: SectionHeaderComponent, rhs: SectionHeaderComponent) -> Bool { @@ -30,12 +36,16 @@ final class SectionHeaderComponent: Component { if lhs.title != rhs.title { return false } + if lhs.actionTitle != rhs.actionTitle { + return false + } return true } final class View: UIView { private let title = ComponentView() private let backgroundView: BlurredBackgroundView + private let action = ComponentView() private var component: SectionHeaderComponent? private weak var state: EmptyComponentState? @@ -95,6 +105,32 @@ final class SectionHeaderComponent: Component { } } + if let actionTitle = component.actionTitle { + let actionSize = self.action.update( + transition: .immediate, + component: AnyComponent( + Button(content: AnyComponent(MultilineTextComponent( + text: .plain(NSAttributedString(string: actionTitle, font: Font.regular(13.0), textColor: component.theme.list.itemSecondaryTextColor)) + )), action: { [weak self] in + if let self, let component = self.component { + component.action?() + } + }) + ), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + if let view = self.action.view { + if view.superview == nil { + self.addSubview(view) + } + let actionFrame = CGRect(origin: CGPoint(x: availableSize.width - leftInset - titleSize.width, y: floor((height - titleSize.height) / 2.0)), size: actionSize) + view.frame = actionFrame + } + } else if self.action.view?.superview != nil { + self.action.view?.removeFromSuperview() + } + let size = CGSize(width: availableSize.width, height: height) transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(), size: size)) diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index 534fd87bf8..7edbb50678 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -307,6 +307,10 @@ final class ShareWithPeersScreenComponent: Component { private var savedSelectedPeers: [EnginePeer.Id] = [] private var selectedPeers: [EnginePeer.Id] = [] + private var selectedGroups: [EnginePeer.Id] = [] + + private var peersMap: [EnginePeer.Id: EnginePeer] = [:] + private var selectedCategories = Set() private var selectedOptions = Set() @@ -557,6 +561,132 @@ final class ShareWithPeersScreenComponent: Component { controller.present(tooltipScreen, in: .window(.root)) } + private func toggleGroupPeer(_ peer: EnginePeer) { + guard let component = self.component, let environment = self.environment, let controller = self.environment?.controller() else { + return + } + + let countLimit = 200 + var groupTooLarge = false + if case let .legacyGroup(group) = peer, group.participantCount > countLimit { + groupTooLarge = true + } else if let stateValue = self.effectiveStateValue, let count = stateValue.participants[peer.id], count > countLimit { + groupTooLarge = true + } + + let showCountLimitAlert = { [weak controller, weak environment, weak component] in + guard let controller, let environment, let component else { + return + } + let alertController = textAlertController( + context: component.context, + forceTheme: defaultDarkColorPresentationTheme, + title: environment.strings.Story_Privacy_GroupTooLarge, + text: environment.strings.Story_Privacy_GroupParticipantsLimit, + actions: [ + TextAlertAction(type: .defaultAction, title: environment.strings.Common_OK, action: {}) + ], + actionLayout: .vertical + ) + controller.present(alertController, in: .window(.root)) + } + + if groupTooLarge { + showCountLimitAlert() + return + } + + var append = false + if let index = self.selectedGroups.firstIndex(of: peer.id) { + self.selectedGroups.remove(at: index) + } else { + self.selectedGroups.append(peer.id) + append = true + } + + let processPeers: ([EnginePeer]) -> Void = { [weak self] peers in + guard let self else { + return + } + + var peerIds = Set() + for peer in peers { + self.peersMap[peer.id] = peer + peerIds.insert(peer.id) + } + var existingPeerIds = Set() + for peerId in self.selectedPeers { + existingPeerIds.insert(peerId) + } + if append { + if peers.count > countLimit { + showCountLimitAlert() + return + } + for peer in peers { + if !existingPeerIds.contains(peer.id) { + self.selectedPeers.append(peer.id) + existingPeerIds.insert(peer.id) + } + } + } else { + self.selectedPeers = self.selectedPeers.filter { !peerIds.contains($0) } + } + let transition = Transition(animation: .curve(duration: 0.35, curve: .spring)) + self.state?.updated(transition: transition) + } + + let context = component.context + if peer.id.namespace == Namespaces.Peer.CloudGroup { + let _ = (context.engine.data.subscribe( + TelegramEngine.EngineData.Item.Peer.LegacyGroupParticipants(id: peer.id) + ) + |> mapToSignal { participants -> Signal<[EnginePeer], NoError> in + if case let .known(participants) = participants { + return context.engine.data.get( + EngineDataMap(participants.map { TelegramEngine.EngineData.Item.Peer.Peer(id: $0.peerId) }) + ) + |> map { peers in + var result: [EnginePeer] = [] + for participant in participants { + if let peer = peers[participant.peerId], let peer, peer.id != context.account.peerId { + result.append(peer) + } + } + return result + } + } else { + let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: peer.id).start() + return .complete() + } + } + |> take(1) + |> deliverOnMainQueue).start(next: { peers in + processPeers(peers) + }) + } else if peer.id.namespace == Namespaces.Peer.CloudChannel { + let participants: Signal<[EnginePeer], NoError> = Signal { subscriber in + let (disposable, _) = context.peerChannelMemberCategoriesContextsManager.recent(engine: context.engine, postbox: context.account.postbox, network: context.account.network, accountPeerId: context.account.peerId, peerId: peer.id, requestUpdate: true, updated: { list in + var peers: [EnginePeer] = [] + for item in list.list { + peers.append(EnginePeer(item.peer)) + } + if !peers.isEmpty { + subscriber.putNext(peers) + subscriber.putCompletion() + } + }) + return disposable + } + + let _ = (participants + |> take(1) + |> deliverOnMainQueue).start(next: { peers in + processPeers(peers) + }) + } + } + private func updateScrolling(transition: Transition) { guard let component = self.component, let environment = self.environment, let itemLayout = self.itemLayout else { return @@ -665,14 +795,22 @@ final class ShareWithPeersScreenComponent: Component { component: AnyComponent(SectionHeaderComponent( theme: environment.theme, style: itemLayout.style, - title: sectionTitle + title: sectionTitle, + actionTitle: (section.id == 1 && !self.selectedPeers.isEmpty) ? environment.strings.Contacts_DeselectAll : nil, + action: { [weak self] in + if let self { + self.selectedPeers = [] + self.selectedGroups = [] + let transition = Transition(animation: .curve(duration: 0.35, curve: .spring)) + self.state?.updated(transition: transition) + } + } )), environment: {}, containerSize: sectionHeaderFrame.size ) if let sectionHeaderView = sectionHeader.view { if sectionHeaderView.superview == nil { - sectionHeaderView.isUserInteractionEnabled = false self.scrollContentClippingView.addSubview(sectionHeaderView) } if minSectionHeader == nil { @@ -736,7 +874,8 @@ final class ShareWithPeersScreenComponent: Component { self.selectedCategories.removeAll() self.selectedCategories.insert(categoryId) - if self.selectedPeers.isEmpty && categoryId == .selectedContacts { + let closeFriends = self.component?.stateContext.stateValue?.closeFriendsPeers ?? [] + if categoryId == .selectedContacts && self.selectedPeers.isEmpty { component.editCategory( EngineStoryPrivacy(base: .nobody, additionallyIncludePeers: []), self.selectedOptions.contains(.screenshot), @@ -744,6 +883,14 @@ final class ShareWithPeersScreenComponent: Component { ) controller.dismissAllTooltips() controller.dismiss() + } else if categoryId == .closeFriends && closeFriends.isEmpty { + component.editCategory( + EngineStoryPrivacy(base: .closeFriends, additionallyIncludePeers: []), + self.selectedOptions.contains(.screenshot), + self.selectedOptions.contains(.pin) + ) + controller.dismissAllTooltips() + controller.dismiss() } } self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) @@ -811,6 +958,20 @@ final class ShareWithPeersScreenComponent: Component { self.visibleItems[itemId] = visibleItem } + let subtitle: String? + if case let .legacyGroup(group) = peer { + subtitle = environment.strings.Conversation_StatusMembers(Int32(group.participantCount)) + } else if case .channel = peer { + if let count = stateValue.participants[peer.id] { + subtitle = environment.strings.Conversation_StatusMembers(Int32(count)) + } else { + subtitle = nil + } + } else { + subtitle = nil + } + + let isSelected = self.selectedPeers.contains(peer.id) || self.selectedGroups.contains(peer.id) let _ = visibleItem.update( transition: itemTransition, component: AnyComponent(PeerListItemComponent( @@ -821,19 +982,23 @@ final class ShareWithPeersScreenComponent: Component { sideInset: itemLayout.sideInset, title: peer.displayTitle(strings: environment.strings, displayOrder: .firstLast), peer: peer, - subtitle: nil, + subtitle: subtitle, subtitleAccessory: .none, presence: stateValue.presences[peer.id], - selectionState: .editing(isSelected: self.selectedPeers.contains(peer.id), isTinted: false), + selectionState: .editing(isSelected: isSelected, isTinted: false), hasNext: true, action: { [weak self] peer in guard let self else { return } - if let index = self.selectedPeers.firstIndex(of: peer.id) { - self.selectedPeers.remove(at: index) + if peer.id.isGroupOrChannel { + self.toggleGroupPeer(peer) } else { - self.selectedPeers.append(peer.id) + if let index = self.selectedPeers.firstIndex(of: peer.id) { + self.selectedPeers.remove(at: index) + } else { + self.selectedPeers.append(peer.id) + } } let transition = Transition(animation: .curve(duration: 0.35, curve: .spring)) @@ -1244,9 +1409,20 @@ final class ShareWithPeersScreenComponent: Component { var tokens: [TokenListTextField.Token] = [] for peerId in self.selectedPeers { - guard let stateValue = self.defaultStateValue, let peer = stateValue.peers.first(where: { $0.id == peerId }) else { + guard let stateValue = self.defaultStateValue else { continue } + var peer: EnginePeer? + if let peerValue = self.peersMap[peerId] { + peer = peerValue + } else if let peerValue = stateValue.peers.first(where: { $0.id == peerId }) { + peer = peerValue + } + + guard let peer else { + continue + } + tokens.append(TokenListTextField.Token( id: AnyHashable(peerId), title: peer.compactDisplayTitle, @@ -1440,7 +1616,7 @@ final class ShareWithPeersScreenComponent: Component { actionButtonTitle = environment.strings.Story_Privacy_PostStory } case .chats: - title = "" + title = environment.strings.Story_Privacy_CategorySelectedContacts case let .contacts(category): switch category { case .closeFriends: @@ -1507,6 +1683,13 @@ final class ShareWithPeersScreenComponent: Component { transition.setFrame(layer: self.navigationSeparatorLayer, frame: CGRect(origin: CGPoint(x: containerSideInset, y: navigationHeight), size: CGSize(width: containerWidth, height: UIScreenPixel))) + let badge: Int + if case .stories = component.stateContext.subject { + badge = 0 + } else { + badge = self.selectedPeers.count + } + let actionButtonSize = self.actionButton.update( transition: transition, component: AnyComponent(ButtonComponent( @@ -1519,7 +1702,7 @@ final class ShareWithPeersScreenComponent: Component { id: actionButtonTitle, component: AnyComponent(ButtonTextContentComponent( text: actionButtonTitle, - badge: 0, + badge: badge, textColor: environment.theme.list.itemCheckColors.foregroundColor, badgeBackground: environment.theme.list.itemCheckColors.foregroundColor, badgeForeground: environment.theme.list.itemCheckColors.fillColor @@ -1755,15 +1938,18 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { public final class State { let peers: [EnginePeer] let presences: [EnginePeer.Id: EnginePeer.Presence] + let participants: [EnginePeer.Id: Int] let closeFriendsPeers: [EnginePeer] fileprivate init( peers: [EnginePeer], presences: [EnginePeer.Id: EnginePeer.Presence], + participants: [EnginePeer.Id: Int], closeFriendsPeers: [EnginePeer] ) { self.peers = peers self.presences = presences + self.participants = participants self.closeFriendsPeers = closeFriendsPeers } } @@ -1820,6 +2006,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { let state = State( peers: peers.compactMap { $0 }, presences: [:], + participants: [:], closeFriendsPeers: closeFriends ) self.stateValue = state @@ -1828,16 +2015,36 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { self.readySubject.set(true) }) case .chats: - self.stateDisposable = (context.engine.messages.chatList(group: .root, count: 200) - |> deliverOnMainQueue).start(next: { [weak self] chatList in + self.stateDisposable = (combineLatest( + context.engine.messages.chatList(group: .root, count: 200) |> take(1), + context.engine.data.get(TelegramEngine.EngineData.Item.Contacts.List(includePresences: true)) + ) + |> mapToSignal { chatList, contacts -> Signal<(EngineChatList, EngineContactList, [EnginePeer.Id: Optional]), NoError> in + return context.engine.data.subscribe( + EngineDataMap(chatList.items.map(\.renderedPeer.peerId).map(TelegramEngine.EngineData.Item.Peer.ParticipantCount.init)) + ) + |> map { participantCountMap -> (EngineChatList, EngineContactList, [EnginePeer.Id: Optional]) in + return (chatList, contacts, participantCountMap) + } + } + |> deliverOnMainQueue).start(next: { [weak self] chatList, contacts, participantCounts in guard let self else { return } + var participants: [EnginePeer.Id: Int] = [:] + for (key, value) in participantCounts { + if let value { + participants[key] = value + } + } + + var existingIds = Set() var selectedPeers: [EnginePeer] = [] for item in chatList.items.reversed() { if self.initialPeerIds.contains(item.renderedPeer.peerId), let peer = item.renderedPeer.peer { selectedPeers.append(peer) + existingIds.insert(peer.id) } } @@ -1847,17 +2054,50 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { } var peers: [EnginePeer] = [] - peers = chatList.items.filter { !self.initialPeerIds.contains($0.renderedPeer.peerId) && $0.renderedPeer.peerId != context.account.peerId }.reversed().compactMap { $0.renderedPeer.peer } + peers = chatList.items.filter { peer in + if let peer = peer.renderedPeer.peer { + if self.initialPeerIds.contains(peer.id) { + return false + } + if peer.id == context.account.peerId { + return false + } + if peer.isService { + return false + } + if case let .channel(channel) = peer { + if channel.isForum { + return false + } + if case .broadcast = channel.info { + return false + } + } + return true + } else { + return false + } + }.reversed().compactMap { $0.renderedPeer.peer } + for peer in peers { + existingIds.insert(peer.id) + } peers.insert(contentsOf: selectedPeers, at: 0) let state = State( peers: peers, presences: presences, + participants: participants, closeFriendsPeers: [] ) self.stateValue = state self.stateSubject.set(.single(state)) + for peer in state.peers { + if case let .channel(channel) = peer, participants[channel.id] == nil { + let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: channel.id).start() + } + } + self.readySubject.set(true) }) case let .contacts(base): @@ -1908,6 +2148,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { let state = State( peers: peers, presences: contactList.presences, + participants: [:], closeFriendsPeers: [] ) @@ -1917,7 +2158,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { self.readySubject.set(true) }) case let .search(query, onlyContacts): - let signal: Signal<[EngineRenderedPeer], NoError> + let signal: Signal<([EngineRenderedPeer], [EnginePeer.Id: Optional]), NoError> if onlyContacts { signal = combineLatest( context.engine.contacts.searchLocalPeers(query: query), @@ -1925,17 +2166,32 @@ 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.ParticipantCount.init)) + ) + |> map { participantCountMap -> ([EngineRenderedPeer], [EnginePeer.Id: Optional]) in + return (peers, participantCountMap) + } + } } self.stateDisposable = (signal - |> deliverOnMainQueue).start(next: { [weak self] peers in + |> deliverOnMainQueue).start(next: { [weak self] peers, participantCounts in guard let self else { return } - + + var participants: [EnginePeer.Id: Int] = [:] + for (key, value) in participantCounts { + if let value { + participants[key] = value + } + } + let state = State( peers: peers.compactMap { $0.peer }.filter { peer in if case let .user(user) = peer { @@ -1946,16 +2202,31 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { } else { return true } + } else if case let .channel(channel) = peer { + if channel.isForum { + return false + } + if case .broadcast = channel.info { + return false + } + return true } else { - return false + return true } }, presences: [:], + participants: participants, closeFriendsPeers: [] ) self.stateValue = state self.stateSubject.set(.single(state)) + for peer in state.peers { + if case let .channel(channel) = peer, participants[channel.id] == nil { + let _ = context.engine.peers.fetchAndUpdateCachedPeerData(peerId: channel.id).start() + } + } + self.readySubject.set(true) }) } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift index 241d8be961..6b2694c84b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryChatContent.swift @@ -191,8 +191,8 @@ public final class StoryContentContextImpl: StoryContentContext { isPublic: item.privacy.base == .everyone, isPending: true, isCloseFriends: item.privacy.base == .closeFriends, - isContacts: item.privacy.base == .contacts && item.privacy.additionallyIncludePeers.isEmpty, - isSelectedContacts: item.privacy.base == .contacts && !item.privacy.additionallyIncludePeers.isEmpty, + isContacts: item.privacy.base == .contacts, + isSelectedContacts: item.privacy.base == .nobody, isForwardingDisabled: false, isEdited: false )) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index b01be680ee..5c1780f70a 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -3209,7 +3209,7 @@ public final class StoryItemSetContainerComponent: Component { text = component.strings.Story_PrivacyTooltipCloseFriends } else if privacy.base == .nobody { if !privacy.additionallyIncludePeers.isEmpty { - text = component.strings.Story_PrivacyTooltipSelectedContacts + text = component.strings.Story_PrivacyTooltipSelectedContactsCount("\(privacy.additionallyIncludePeers.count)").string } else { text = component.strings.Story_PrivacyTooltipNobody } diff --git a/submodules/TelegramUI/Components/TokenListTextField/Sources/EditableTokenListNode.swift b/submodules/TelegramUI/Components/TokenListTextField/Sources/EditableTokenListNode.swift index 81bca64a7e..99190021a3 100644 --- a/submodules/TelegramUI/Components/TokenListTextField/Sources/EditableTokenListNode.swift +++ b/submodules/TelegramUI/Components/TokenListTextField/Sources/EditableTokenListNode.swift @@ -450,7 +450,7 @@ final class EditableTokenListNode: ASDisplayNode, UITextFieldDelegate { let previousContentHeight = self.scrollNode.view.contentSize.height let contentHeight = currentOffset.y + 29.0 + verticalInset - let nodeHeight = min(contentHeight, 110.0) + let nodeHeight = min(contentHeight, 140.0) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: width, height: UIScreenPixel))) transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: nodeHeight))) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 45e313eb82..ac285db1e5 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -13054,7 +13054,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G if !value || concealed || botApp.flags.contains(.notActivated) { let context = self.context - let controller = webAppLaunchConfirmationController(context: context, updatedPresentationData: self.updatedPresentationData, peer: botPeer, requestWriteAccess: !botApp.flags.contains(.notActivated) && botApp.flags.contains(.requiresWriteAccess), completion: { allowWrite in + let controller = webAppLaunchConfirmationController(context: context, updatedPresentationData: self.updatedPresentationData, peer: botPeer, requestWriteAccess: botApp.flags.contains(.notActivated) && botApp.flags.contains(.requiresWriteAccess), completion: { allowWrite in let _ = ApplicationSpecificNotice.setBotGameNotice(accountManager: context.sharedContext.accountManager, peerId: botPeer.id).start() openBotApp(allowWrite) }, showMore: { [weak self] in From 97d22eff773c7015989dbd27d254e7e930d32eb6 Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Wed, 26 Jul 2023 20:07:19 +0200 Subject: [PATCH 2/6] Bump version --- versions.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/versions.json b/versions.json index 096532970c..68347b89b1 100644 --- a/versions.json +++ b/versions.json @@ -1,5 +1,5 @@ { - "app": "9.6.5", + "app": "9.6.6", "bazel": "6.1.1", "xcode": "14.2" } From 4a0e81c712a47f27630d444ea0027e6d6d9cf6c0 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Wed, 26 Jul 2023 23:31:03 +0400 Subject: [PATCH 3/6] Add story reply transition view --- .../TelegramUI/Sources/ChatController.swift | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index ac285db1e5..dac473a2ac 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -4585,13 +4585,32 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if let result = itemNode.targetForStoryTransition(id: storyId) { + result.isHidden = true transitionOut = StoryContainerScreen.TransitionOut( destinationView: result, - transitionView: nil, + transitionView: StoryContainerScreen.TransitionView( + makeView: { [weak result] in + let parentView = UIView() + if let copyView = result?.snapshotContentTree(unhide: true) { + parentView.addSubview(copyView) + } + return parentView + }, + updateView: { copyView, state, transition in + guard let view = copyView.subviews.first else { + return + } + let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress) + transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) + transition.setScale(view: view, scale: size.width / state.destinationSize.width) + }, + insertCloneTransitionView: nil + ), destinationRect: result.bounds, destinationCornerRadius: 2.0, destinationIsAvatar: false, - completed: { + completed: { [weak result] in + result?.isHidden = false } ) } From a549222394b11b9b1c8d241343601de03d3848b6 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Tue, 25 Jul 2023 01:45:44 +0400 Subject: [PATCH 4/6] Cherry pick commit --- .../Telegram-iOS/en.lproj/Localizable.strings | 3 - .../Sources/ChatListController.swift | 18 +- .../Components/BalancedTextComponent/BUILD | 20 ++ .../Sources/BalancedTextComponent.swift | 209 +++++++++++++++++ .../Sources/ViewControllerComponent.swift | 8 +- submodules/Display/Source/TextNode.swift | 22 +- .../Display/Source/TooltipController.swift | 6 +- .../Source/TooltipControllerNode.swift | 4 +- .../Sources/MediaPickerScreen.swift | 16 ++ .../Sources/MediaPlayerFramePreview.swift | 2 +- .../UniversalSoftwareVideoSource.swift | 95 +++++--- .../Sources/MTRequestMessageService.m | 6 +- .../Sources/PremiumIntroScreen.swift | 1 + .../Sources/ApiUtils/TelegramMediaFile.swift | 2 +- .../Sources/ChatEntityKeyboardInputNode.swift | 13 +- .../Sources/PeerInfoVisualMediaPaneNode.swift | 8 +- .../Sources/StoryItemContentComponent.swift | 2 +- .../StoryItemSetContainerComponent.swift | 65 +++-- ...StoryItemSetContainerViewSendMessage.swift | 30 ++- .../StoryItemSetViewListComponent.swift | 2 +- .../PanelGradient.imageset/Contents.json | 2 +- .../smoothGradient 0.4.png | Bin 0 -> 1353 bytes .../smoothGradient 0.6.png | Bin 1781 -> 0 bytes .../ChatMessageInteractiveFileNode.swift | 16 +- .../Sources/FetchCachedRepresentations.swift | 31 ++- .../Sources/PeerInfoGifPaneNode.swift | 8 +- submodules/TooltipUI/BUILD | 1 + .../TooltipUI/Sources/TooltipScreen.swift | 222 ++++++++++-------- 28 files changed, 631 insertions(+), 181 deletions(-) create mode 100644 submodules/Components/BalancedTextComponent/BUILD create mode 100644 submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift create mode 100644 submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/smoothGradient 0.4.png delete mode 100644 submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/smoothGradient 0.6.png diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 89e42da1a3..b4858876e5 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -9394,8 +9394,6 @@ Sorry for the inconvenience."; "Notification.Story" = "Story"; -"ChatList.StoryFeedTooltip" = "Tap above to view updates\nfrom %@"; - "StoryFeed.ContextAddStory" = "Add Story"; "StoryFeed.ContextSavedStories" = "Saved Stories"; "StoryFeed.ContextArchivedStories" = "Archived Stories"; @@ -9549,7 +9547,6 @@ Sorry for the inconvenience."; "Story.ContextDeleteStory" = "Delete Story"; -"Story.TooltipPrivacyCloseFriends" = "You are seeing this story because **%@** added you\nto their list of Close Friends."; "Story.TooltipPrivacyContacts" = "Only **%@'s** contacts can view this story."; "Story.TooltipPrivacySelectedContacts" = "Only some contacts **%@** selected can view this story."; "Story.ToastViewInChat" = "View in Chat"; diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 89c2e05b0b..401409aa43 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2013,15 +2013,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - let text: String = self.presentationData.strings.ChatList_StoryFeedTooltip(itemListString).string + let text: String = self.presentationData.strings.ChatList_StoryFeedTooltipUsers(itemListString).string - let tooltipController = TooltipController(content: .text(text), baseFontSize: self.presentationData.listsFontSize.baseDisplaySize, timeout: 30.0, dismissByTapOutside: true, dismissImmediatelyOnLayoutUpdate: true, padding: 6.0, innerPadding: UIEdgeInsets(top: 2.0, left: 3.0, bottom: 2.0, right: 3.0)) - self.present(tooltipController, in: .current, with: TooltipControllerPresentationArguments(sourceNodeAndRect: { [weak self] in - guard let self else { - return nil + let tooltipScreen = TooltipScreen( + account: self.context.account, + sharedContext: self.context.sharedContext, + text: .markdown(text: text), + balancedTextLayout: true, + style: .default, + location: TooltipScreen.Location.point(self.displayNode.view.convert(absoluteFrame.insetBy(dx: 0.0, dy: 0.0).offsetBy(dx: 0.0, dy: 4.0), to: nil).offsetBy(dx: 1.0, dy: 2.0), .top), displayDuration: .infinite, shouldDismissOnTouch: { _, _ in + return .dismiss(consume: false) } - return (self.displayNode, absoluteFrame.insetBy(dx: 0.0, dy: 0.0).offsetBy(dx: 0.0, dy: 4.0)) - })) + ) + self.present(tooltipScreen, in: .current) #if !DEBUG let _ = ApplicationSpecificNotice.setDisplayChatListStoriesTooltip(accountManager: self.context.sharedContext.accountManager).start() diff --git a/submodules/Components/BalancedTextComponent/BUILD b/submodules/Components/BalancedTextComponent/BUILD new file mode 100644 index 0000000000..35ba9d1832 --- /dev/null +++ b/submodules/Components/BalancedTextComponent/BUILD @@ -0,0 +1,20 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "BalancedTextComponent", + module_name = "BalancedTextComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display:Display", + "//submodules/ComponentFlow:ComponentFlow", + "//submodules/Markdown:Markdown", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift b/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift new file mode 100644 index 0000000000..14802b61aa --- /dev/null +++ b/submodules/Components/BalancedTextComponent/Sources/BalancedTextComponent.swift @@ -0,0 +1,209 @@ +import Foundation +import UIKit +import ComponentFlow +import Display +import Markdown + +public final class BalancedTextComponent: Component { + public enum TextContent: Equatable { + case plain(NSAttributedString) + case markdown(text: String, attributes: MarkdownAttributes) + } + + public let text: TextContent + public let balanced: Bool + public let horizontalAlignment: NSTextAlignment + public let verticalAlignment: TextVerticalAlignment + public let truncationType: CTLineTruncationType + public let maximumNumberOfLines: Int + public let lineSpacing: CGFloat + public let cutout: TextNodeCutout? + public let insets: UIEdgeInsets + public let textShadowColor: UIColor? + public let textShadowBlur: CGFloat? + public let textStroke: (UIColor, CGFloat)? + public let highlightColor: UIColor? + public let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? + public let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? + public let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? + + public init( + text: TextContent, + balanced: Bool = true, + horizontalAlignment: NSTextAlignment = .natural, + verticalAlignment: TextVerticalAlignment = .top, + truncationType: CTLineTruncationType = .end, + maximumNumberOfLines: Int = 1, + lineSpacing: CGFloat = 0.0, + cutout: TextNodeCutout? = nil, + insets: UIEdgeInsets = UIEdgeInsets(), + textShadowColor: UIColor? = nil, + textShadowBlur: CGFloat? = nil, + textStroke: (UIColor, CGFloat)? = nil, + highlightColor: UIColor? = nil, + highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = nil, + tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil, + longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = nil + ) { + self.text = text + self.balanced = balanced + self.horizontalAlignment = horizontalAlignment + self.verticalAlignment = verticalAlignment + self.truncationType = truncationType + self.maximumNumberOfLines = maximumNumberOfLines + self.lineSpacing = lineSpacing + self.cutout = cutout + self.insets = insets + self.textShadowColor = textShadowColor + self.textShadowBlur = textShadowBlur + self.textStroke = textStroke + self.highlightColor = highlightColor + self.highlightAction = highlightAction + self.tapAction = tapAction + self.longTapAction = longTapAction + } + + public static func ==(lhs: BalancedTextComponent, rhs: BalancedTextComponent) -> Bool { + if lhs.text != rhs.text { + return false + } + if lhs.balanced != rhs.balanced { + return false + } + if lhs.horizontalAlignment != rhs.horizontalAlignment { + return false + } + if lhs.verticalAlignment != rhs.verticalAlignment { + return false + } + if lhs.truncationType != rhs.truncationType { + return false + } + if lhs.maximumNumberOfLines != rhs.maximumNumberOfLines { + return false + } + if lhs.lineSpacing != rhs.lineSpacing { + return false + } + if lhs.cutout != rhs.cutout { + return false + } + if lhs.insets != rhs.insets { + return false + } + + if let lhsTextShadowColor = lhs.textShadowColor, let rhsTextShadowColor = rhs.textShadowColor { + if !lhsTextShadowColor.isEqual(rhsTextShadowColor) { + return false + } + } else if (lhs.textShadowColor != nil) != (rhs.textShadowColor != nil) { + return false + } + if lhs.textShadowBlur != rhs.textShadowBlur { + return false + } + + if let lhsTextStroke = lhs.textStroke, let rhsTextStroke = rhs.textStroke { + if !lhsTextStroke.0.isEqual(rhsTextStroke.0) { + return false + } + if lhsTextStroke.1 != rhsTextStroke.1 { + return false + } + } else if (lhs.textShadowColor != nil) != (rhs.textShadowColor != nil) { + return false + } + + if let lhsHighlightColor = lhs.highlightColor, let rhsHighlightColor = rhs.highlightColor { + if !lhsHighlightColor.isEqual(rhsHighlightColor) { + return false + } + } else if (lhs.highlightColor != nil) != (rhs.highlightColor != nil) { + return false + } + + return true + } + + public final class View: UIView { + private let textView: ImmediateTextView + + override public init(frame: CGRect) { + self.textView = ImmediateTextView() + + super.init(frame: frame) + + self.addSubview(self.textView) + } + + required public init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + public func attributeSubstring(name: String, index: Int) -> (String, String)? { + return self.textView.attributeSubstring(name: name, index: index) + } + + public func update(component: BalancedTextComponent, availableSize: CGSize, transition: Transition) -> CGSize { + let attributedString: NSAttributedString + switch component.text { + case let .plain(string): + attributedString = string + case let .markdown(text, attributes): + attributedString = parseMarkdownIntoAttributedString(text, attributes: attributes) + } + + self.textView.attributedText = attributedString + self.textView.maximumNumberOfLines = component.maximumNumberOfLines + self.textView.truncationType = component.truncationType + self.textView.textAlignment = component.horizontalAlignment + self.textView.verticalAlignment = component.verticalAlignment + self.textView.lineSpacing = component.lineSpacing + self.textView.cutout = component.cutout + self.textView.insets = component.insets + self.textView.textShadowColor = component.textShadowColor + self.textView.textShadowBlur = component.textShadowBlur + self.textView.textStroke = component.textStroke + self.textView.linkHighlightColor = component.highlightColor + self.textView.highlightAttributeAction = component.highlightAction + self.textView.tapAttributeAction = component.tapAction + self.textView.longTapAttributeAction = component.longTapAction + + var bestSize: (availableWidth: CGFloat, info: TextNodeLayout) + + let info = self.textView.updateLayoutFullInfo(availableSize) + bestSize = (availableSize.width, info) + + if component.balanced && info.numberOfLines > 1 { + let measureIncrement = 8.0 + var measureWidth = info.size.width + measureWidth -= measureIncrement + while measureWidth > 0.0 { + let otherInfo = self.textView.updateLayoutFullInfo(CGSize(width: measureWidth, height: availableSize.height)) + if otherInfo.numberOfLines > bestSize.info.numberOfLines { + break + } + if (otherInfo.size.width - otherInfo.trailingLineWidth) < (bestSize.info.size.width - bestSize.info.trailingLineWidth) { + bestSize = (measureWidth, otherInfo) + } + + measureWidth -= measureIncrement + } + + let bestInfo = self.textView.updateLayoutFullInfo(CGSize(width: bestSize.availableWidth, height: availableSize.height)) + bestSize = (availableSize.width, bestInfo) + } + + self.textView.frame = CGRect(origin: CGPoint(), size: bestSize.info.size) + return bestSize.info.size + } + } + + public func makeView() -> View { + return View() + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} diff --git a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift index 39706976f6..ccbee0a11a 100644 --- a/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift +++ b/submodules/Components/ViewControllerComponent/Sources/ViewControllerComponent.swift @@ -223,6 +223,8 @@ open class ViewControllerComponentContainer: ViewController { private var presentationDataDisposable: Disposable? public private(set) var validLayout: ContainerViewLayout? + public var wasDismissed: (() -> Void)? + public init(context: AccountContext, component: C, navigationBarAppearance: NavigationBarAppearance, statusBarStyle: StatusBarStyle = .default, presentationMode: PresentationMode = .default, theme: Theme = .default) where C.EnvironmentType == ViewControllerComponentContainer.Environment { self.context = context self.component = AnyComponent(component) @@ -304,7 +306,11 @@ open class ViewControllerComponentContainer: ViewController { } open override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { - super.dismiss(animated: flag, completion: completion) + let wasDismissed = self.wasDismissed + super.dismiss(animated: flag, completion: { + completion?() + wasDismissed?() + }) } fileprivate var forceNextUpdate = false diff --git a/submodules/Display/Source/TextNode.swift b/submodules/Display/Source/TextNode.swift index e1fdf727ac..32517738df 100644 --- a/submodules/Display/Source/TextNode.swift +++ b/submodules/Display/Source/TextNode.swift @@ -1279,7 +1279,27 @@ open class TextNode: ASDisplayNode { coreTextLine = originalLine } } else { - coreTextLine = CTLineCreateTruncatedLine(originalLine, max(1.0, Double(lineConstrainedSize.width) - truncationTokenWidth), truncationType, truncationToken) ?? truncationToken + if customTruncationToken != nil { + let coreTextLine1 = CTLineCreateTruncatedLine(originalLine, max(1.0, Double(lineConstrainedSize.width)), truncationType, truncationToken) ?? truncationToken + let runs = (CTLineGetGlyphRuns(coreTextLine1) as [AnyObject]) as! [CTRun] + var hasTruncationToken = false + for run in runs { + let runRange = CTRunGetStringRange(run) + if runRange.location + runRange.length >= nsString.length { + hasTruncationToken = true + break + } + } + + if hasTruncationToken { + coreTextLine = coreTextLine1 + } else { + let coreTextLine2 = CTLineCreateTruncatedLine(originalLine, max(1.0, Double(lineConstrainedSize.width) - truncationTokenWidth), truncationType, truncationToken) ?? truncationToken + coreTextLine = coreTextLine2 + } + } else { + coreTextLine = CTLineCreateTruncatedLine(originalLine, max(1.0, Double(lineConstrainedSize.width)), truncationType, truncationToken) ?? truncationToken + } let runs = (CTLineGetGlyphRuns(coreTextLine) as [AnyObject]) as! [CTRun] for run in runs { let runAttributes: NSDictionary = CTRunGetAttributes(run) diff --git a/submodules/Display/Source/TooltipController.swift b/submodules/Display/Source/TooltipController.swift index be5d52bff7..25605bcd66 100644 --- a/submodules/Display/Source/TooltipController.swift +++ b/submodules/Display/Source/TooltipController.swift @@ -100,6 +100,7 @@ open class TooltipController: ViewController, StandalonePresentableController { public private(set) var content: TooltipControllerContent private let baseFontSize: CGFloat + private let balancedTextLayout: Bool open func updateContent(_ content: TooltipControllerContent, animated: Bool, extendTimer: Bool, arrowOnBottom: Bool = true) { if self.content != content { @@ -130,9 +131,10 @@ open class TooltipController: ViewController, StandalonePresentableController { public var dismissed: ((Bool) -> Void)? - public init(content: TooltipControllerContent, baseFontSize: CGFloat, timeout: Double = 2.0, dismissByTapOutside: Bool = false, dismissByTapOutsideSource: Bool = false, dismissImmediatelyOnLayoutUpdate: Bool = false, arrowOnBottom: Bool = true, padding: CGFloat = 8.0, innerPadding: UIEdgeInsets = UIEdgeInsets()) { + public init(content: TooltipControllerContent, baseFontSize: CGFloat, balancedTextLayout: Bool = false, timeout: Double = 2.0, dismissByTapOutside: Bool = false, dismissByTapOutsideSource: Bool = false, dismissImmediatelyOnLayoutUpdate: Bool = false, arrowOnBottom: Bool = true, padding: CGFloat = 8.0, innerPadding: UIEdgeInsets = UIEdgeInsets()) { self.content = content self.baseFontSize = baseFontSize + self.balancedTextLayout = balancedTextLayout self.timeout = timeout self.dismissByTapOutside = dismissByTapOutside self.dismissByTapOutsideSource = dismissByTapOutsideSource @@ -155,7 +157,7 @@ open class TooltipController: ViewController, StandalonePresentableController { } override open func loadDisplayNode() { - self.displayNode = TooltipControllerNode(content: self.content, baseFontSize: self.baseFontSize, dismiss: { [weak self] tappedInside in + self.displayNode = TooltipControllerNode(content: self.content, baseFontSize: self.baseFontSize, balancedTextLayout: self.balancedTextLayout, dismiss: { [weak self] tappedInside in self?.dismiss(tappedInside: tappedInside) }, dismissByTapOutside: self.dismissByTapOutside, dismissByTapOutsideSource: self.dismissByTapOutsideSource) self.controllerNode.padding = self.padding diff --git a/submodules/Display/Source/TooltipControllerNode.swift b/submodules/Display/Source/TooltipControllerNode.swift index 7122720407..ae7ee4cdf2 100644 --- a/submodules/Display/Source/TooltipControllerNode.swift +++ b/submodules/Display/Source/TooltipControllerNode.swift @@ -4,6 +4,7 @@ import AsyncDisplayKit final class TooltipControllerNode: ASDisplayNode { private let baseFontSize: CGFloat + private let balancedTextLayout: Bool private let dismiss: (Bool) -> Void @@ -25,8 +26,9 @@ final class TooltipControllerNode: ASDisplayNode { private var dismissedByTouchOutside = false private var dismissByTapOutsideSource = false - init(content: TooltipControllerContent, baseFontSize: CGFloat, dismiss: @escaping (Bool) -> Void, dismissByTapOutside: Bool, dismissByTapOutsideSource: Bool) { + init(content: TooltipControllerContent, baseFontSize: CGFloat, balancedTextLayout: Bool, dismiss: @escaping (Bool) -> Void, dismissByTapOutside: Bool, dismissByTapOutsideSource: Bool) { self.baseFontSize = baseFontSize + self.balancedTextLayout = balancedTextLayout self.dismissByTapOutside = dismissByTapOutside self.dismissByTapOutsideSource = dismissByTapOutsideSource diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index aa462a0bc7..8c37d11118 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -781,9 +781,19 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } private weak var currentGalleryController: TGModernGalleryController? + private weak var currentGalleryParentController: ViewController? fileprivate var currentAssetDownloadDisposable = MetaDisposable() + fileprivate func closeGalleryController() { + if let _ = self.currentGalleryController, let currentGalleryParentController = self.currentGalleryParentController { + self.currentGalleryController = nil + self.currentGalleryParentController = nil + + currentGalleryParentController.dismiss(completion: nil) + } + } + fileprivate func cancelAssetDownloads() { guard let downloadManager = self.controller?.downloadManager else { return @@ -868,6 +878,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { strongSelf.controller?.interaction?.sendSelected(result, silently, scheduleTime, false, completion) } }, presentSchedulePicker: controller.presentSchedulePicker, presentTimerPicker: controller.presentTimerPicker, getCaptionPanelView: controller.getCaptionPanelView, present: { [weak self] c, a in + self?.currentGalleryParentController = c self?.controller?.present(c, in: .window(.root), with: a) }, finishedTransitionIn: { [weak self] in self?.openingMedia = false @@ -906,6 +917,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { strongSelf.controller?.interaction?.sendSelected(result, silently, scheduleTime, false, completion) } }, presentSchedulePicker: controller.presentSchedulePicker, presentTimerPicker: controller.presentTimerPicker, getCaptionPanelView: controller.getCaptionPanelView, present: { [weak self] c, a in + self?.currentGalleryParentController = c self?.controller?.present(c, in: .window(.root), with: a, blockInteraction: true) }, finishedTransitionIn: { [weak self] in self?.openingMedia = false @@ -1686,6 +1698,10 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { super.displayNodeDidLoad() } + public func closeGalleryController() { + self.controllerNode.closeGalleryController() + } + private weak var undoOverlayController: UndoOverlayController? private func showSelectionUndo(item: TGMediaSelectableItem) { let scale = min(2.0, UIScreenScale) diff --git a/submodules/MediaPlayer/Sources/MediaPlayerFramePreview.swift b/submodules/MediaPlayer/Sources/MediaPlayerFramePreview.swift index b67019af30..ef00996925 100644 --- a/submodules/MediaPlayer/Sources/MediaPlayerFramePreview.swift +++ b/submodules/MediaPlayer/Sources/MediaPlayerFramePreview.swift @@ -27,7 +27,7 @@ private final class FramePreviewContext { private func initializedPreviewContext(queue: Queue, postbox: Postbox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, fileReference: FileMediaReference) -> Signal, NoError> { return Signal { subscriber in - let source = UniversalSoftwareVideoSource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, fileReference: fileReference) + let source = UniversalSoftwareVideoSource(mediaBox: postbox.mediaBox, source: .file(userLocation: userLocation, userContentType: userContentType, fileReference: fileReference)) let readyDisposable = (source.ready |> filter { $0 }).start(next: { _ in subscriber.putNext(QueueLocalObject(queue: queue, generate: { diff --git a/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift b/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift index 49b303c1b9..0605e03568 100644 --- a/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift +++ b/submodules/MediaPlayer/Sources/UniversalSoftwareVideoSource.swift @@ -21,15 +21,17 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa context.currentReadBytes += readCount let semaphore = DispatchSemaphore(value: 0) - data = context.mediaBox.resourceData(context.fileReference.media.resource, size: context.size, in: requestRange, mode: .partial) + + data = context.mediaBox.resourceData(context.source.resource, size: context.size, in: requestRange, mode: .partial) + let requiredDataIsNotLocallyAvailable = context.requiredDataIsNotLocallyAvailable var fetchedData: Data? let fetchDisposable = MetaDisposable() let isInitialized = context.videoStream != nil || context.automaticallyFetchHeader let mediaBox = context.mediaBox - let userLocation = context.userLocation - let userContentType = context.userContentType - let reference = context.fileReference.resourceReference(context.fileReference.media.resource) + + let source = context.source + let disposable = data.start(next: { result in let (data, isComplete) = result if data.count == readCount || isComplete { @@ -37,7 +39,12 @@ private func readPacketCallback(userData: UnsafeMutableRawPointer?, buffer: Unsa semaphore.signal() } else { if isInitialized { - fetchDisposable.set(fetchedMediaResource(mediaBox: mediaBox, userLocation: userLocation, userContentType: userContentType, reference: reference, ranges: [(requestRange, .maximum)]).start()) + switch source { + case let .file(userLocation, userContentType, fileReference): + fetchDisposable.set(fetchedMediaResource(mediaBox: mediaBox, userLocation: userLocation, userContentType: userContentType, reference: fileReference.resourceReference(fileReference.media.resource), ranges: [(requestRange, .maximum)]).start()) + case .direct: + break + } } requiredDataIsNotLocallyAvailable?() } @@ -100,9 +107,7 @@ private final class SoftwareVideoStream { private final class UniversalSoftwareVideoSourceImpl { fileprivate let mediaBox: MediaBox - fileprivate let userLocation: MediaResourceUserLocation - fileprivate let userContentType: MediaResourceUserContentType - fileprivate let fileReference: FileMediaReference + fileprivate let source: UniversalSoftwareVideoSource.Source fileprivate let size: Int64 fileprivate let automaticallyFetchHeader: Bool @@ -119,16 +124,27 @@ private final class UniversalSoftwareVideoSourceImpl { fileprivate var currentNumberOfReads: Int = 0 fileprivate var currentReadBytes: Int64 = 0 - init?(mediaBox: MediaBox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, fileReference: FileMediaReference, state: ValuePromise, cancelInitialization: Signal, automaticallyFetchHeader: Bool, hintVP9: Bool = false) { - guard let size = fileReference.media.size else { - return nil + init?( + mediaBox: MediaBox, + source: UniversalSoftwareVideoSource.Source, + state: ValuePromise, + cancelInitialization: Signal, + automaticallyFetchHeader: Bool, + hintVP9: Bool = false + ) { + switch source { + case let .file(_, _, fileReference): + guard let size = fileReference.media.size else { + return nil + } + self.size = size + case let .direct(_, sizeValue): + self.size = sizeValue } + self.mediaBox = mediaBox - self.userLocation = userLocation - self.userContentType = userContentType - self.fileReference = fileReference - self.size = size + self.source = source self.automaticallyFetchHeader = automaticallyFetchHeader self.state = state @@ -138,7 +154,15 @@ private final class UniversalSoftwareVideoSourceImpl { let ioBufferSize = 1 * 1024 - guard let avIoContext = FFMpegAVIOContext(bufferSize: Int32(ioBufferSize), opaqueContext: Unmanaged.passUnretained(self).toOpaque(), readPacket: readPacketCallback, writePacket: nil, seek: seekCallback, isSeekable: true) else { + let isSeekable: Bool + switch source { + case .file: + isSeekable = true + case .direct: + isSeekable = false + } + + guard let avIoContext = FFMpegAVIOContext(bufferSize: Int32(ioBufferSize), opaqueContext: Unmanaged.passUnretained(self).toOpaque(), readPacket: readPacketCallback, writePacket: nil, seek: seekCallback, isSeekable: isSeekable) else { return nil } self.avIoContext = avIoContext @@ -295,9 +319,7 @@ private enum UniversalSoftwareVideoSourceState { private final class UniversalSoftwareVideoSourceThreadParams: NSObject { let mediaBox: MediaBox - let userLocation: MediaResourceUserLocation - let userContentType: MediaResourceUserContentType - let fileReference: FileMediaReference + let source: UniversalSoftwareVideoSource.Source let state: ValuePromise let cancelInitialization: Signal let automaticallyFetchHeader: Bool @@ -305,18 +327,14 @@ private final class UniversalSoftwareVideoSourceThreadParams: NSObject { init( mediaBox: MediaBox, - userLocation: MediaResourceUserLocation, - userContentType: MediaResourceUserContentType, - fileReference: FileMediaReference, + source: UniversalSoftwareVideoSource.Source, state: ValuePromise, cancelInitialization: Signal, automaticallyFetchHeader: Bool, hintVP9: Bool ) { self.mediaBox = mediaBox - self.userLocation = userLocation - self.userContentType = userContentType - self.fileReference = fileReference + self.source = source self.state = state self.cancelInitialization = cancelInitialization self.automaticallyFetchHeader = automaticallyFetchHeader @@ -345,7 +363,7 @@ private final class UniversalSoftwareVideoSourceThread: NSObject { let timer = Timer(fireAt: .distantFuture, interval: 0.0, target: UniversalSoftwareVideoSourceThread.self, selector: #selector(UniversalSoftwareVideoSourceThread.none), userInfo: nil, repeats: false) runLoop.add(timer, forMode: .common) - let source = UniversalSoftwareVideoSourceImpl(mediaBox: params.mediaBox, userLocation: params.userLocation, userContentType: params.userContentType, fileReference: params.fileReference, state: params.state, cancelInitialization: params.cancelInitialization, automaticallyFetchHeader: params.automaticallyFetchHeader) + let source = UniversalSoftwareVideoSourceImpl(mediaBox: params.mediaBox, source: params.source, state: params.state, cancelInitialization: params.cancelInitialization, automaticallyFetchHeader: params.automaticallyFetchHeader) Thread.current.threadDictionary["source"] = source while true { @@ -387,6 +405,27 @@ public enum UniversalSoftwareVideoSourceTakeFrameResult { } public final class UniversalSoftwareVideoSource { + public enum Source { + case file( + userLocation: MediaResourceUserLocation, + userContentType: MediaResourceUserContentType, + fileReference: FileMediaReference + ) + case direct( + resource: MediaResource, + size: Int64 + ) + + var resource: MediaResource { + switch self { + case let .file(_, _, fileReference): + return fileReference.media.resource + case let .direct(resource, _): + return resource + } + } + } + private let thread: Thread private let stateValue: ValuePromise = ValuePromise(.initializing, ignoreRepeated: true) private let cancelInitialization: ValuePromise = ValuePromise(false) @@ -403,8 +442,8 @@ public final class UniversalSoftwareVideoSource { } } - public init(mediaBox: MediaBox, userLocation: MediaResourceUserLocation, userContentType: MediaResourceUserContentType, fileReference: FileMediaReference, automaticallyFetchHeader: Bool = false, hintVP9: Bool = false) { - self.thread = Thread(target: UniversalSoftwareVideoSourceThread.self, selector: #selector(UniversalSoftwareVideoSourceThread.entryPoint(_:)), object: UniversalSoftwareVideoSourceThreadParams(mediaBox: mediaBox, userLocation: userLocation, userContentType: userContentType, fileReference: fileReference, state: self.stateValue, cancelInitialization: self.cancelInitialization.get(), automaticallyFetchHeader: automaticallyFetchHeader, hintVP9: hintVP9)) + public init(mediaBox: MediaBox, source: Source, automaticallyFetchHeader: Bool = false, hintVP9: Bool = false) { + self.thread = Thread(target: UniversalSoftwareVideoSourceThread.self, selector: #selector(UniversalSoftwareVideoSourceThread.entryPoint(_:)), object: UniversalSoftwareVideoSourceThreadParams(mediaBox: mediaBox, source: source, state: self.stateValue, cancelInitialization: self.cancelInitialization.get(), automaticallyFetchHeader: automaticallyFetchHeader, hintVP9: hintVP9)) self.thread.name = "UniversalSoftwareVideoSource" self.thread.start() } diff --git a/submodules/MtProtoKit/Sources/MTRequestMessageService.m b/submodules/MtProtoKit/Sources/MTRequestMessageService.m index fed278d302..d8c170f8ca 100644 --- a/submodules/MtProtoKit/Sources/MTRequestMessageService.m +++ b/submodules/MtProtoKit/Sources/MTRequestMessageService.m @@ -120,8 +120,8 @@ { if (request.requestContext != nil) { - //[_dropReponseContexts addObject:[[MTDropResponseContext alloc] initWithDropMessageId:request.requestContext.messageId]]; - //anyNewDropRequests = true; + [_dropReponseContexts addObject:[[MTDropResponseContext alloc] initWithDropMessageId:request.requestContext.messageId]]; + anyNewDropRequests = true; } if (request.requestContext.messageId != 0) { @@ -902,7 +902,7 @@ if (!requestFound) { if (MTLogEnabled()) { - MTLog(@"[MTRequestMessageService#%p response %" PRId64 " didn't match any request]", self, message.messageId); + MTLog(@"[MTRequestMessageService#%p response %" PRId64 " for % " PRId64 " didn't match any request]", self, message.messageId, rpcResultMessage.requestMessageId); } } else if (_requests.count == 0) diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 5e66a39116..b20521e926 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -2792,6 +2792,7 @@ public final class PremiumIntroScreen: ViewControllerComponentContainer { @objc private func cancelPressed() { self.dismiss() + self.wasDismissed?() } public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift index 7fecddfd0b..7ffbcce901 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramMediaFile.swift @@ -181,7 +181,7 @@ func telegramMediaFileFromApiDocument(_ document: Api.Document) -> TelegramMedia } } - return TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: size, fileReference: fileReference.makeData(), fileName: fileNameFromFileAttributes(parsedAttributes)), previewRepresentations: previewRepresentations, videoThumbnails: videoThumbnails, immediateThumbnailData: immediateThumbnail, mimeType: mimeType, size: size, attributes: parsedAttributes) + return TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: size, fileReference: fileReference.makeData(), fileName: fileNameFromFileAttributes(parsedAttributes)), previewRepresentations: previewRepresentations, videoThumbnails: videoThumbnails, immediateThumbnailData: immediateThumbnail, mimeType: mimeType, size: size, attributes: parsedAttributes) case .documentEmpty: return nil } diff --git a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift index 5dccba4682..ee5d65e026 100644 --- a/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift +++ b/submodules/TelegramUI/Components/ChatEntityKeyboardInputNode/Sources/ChatEntityKeyboardInputNode.swift @@ -2425,7 +2425,18 @@ public final class EntityInputView: UIInputView, AttachmentTextInputPanelInputVi replaceImpl?(controller) }) replaceImpl = { [weak controller] c in - controller?.replace(with: c) + guard let controller else { + return + } + if controller.navigationController != nil { + controller.replace(with: c) + } else { + controller.dismiss() + + if let self { + self.presentController?(c) + } + } } strongSelf.presentController?(controller) }), elevatedLayout: false, animateInAsReplacement: false, action: { _ in return false })) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift index be2213e218..3d91718548 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoVisualMediaPaneNode.swift @@ -124,9 +124,11 @@ private final class FrameSequenceThumbnailNode: ASDisplayNode { let source = UniversalSoftwareVideoSource( mediaBox: self.context.account.postbox.mediaBox, - userLocation: userLocation, - userContentType: .other, - fileReference: self.file, + source: .file( + userLocation: userLocation, + userContentType: .other, + fileReference: self.file + ), automaticallyFetchHeader: true ) self.sources.append(source) diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift index f60b7dbf20..55736ddeaa 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemContentComponent.swift @@ -407,7 +407,7 @@ final class StoryItemContentComponent: Component { } progress = min(1.0, progress) - if actualTimestamp < 0.1 { + if actualTimestamp < 0.3 { isBuffering = false } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index 5c1780f70a..efc9868257 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -1392,6 +1392,12 @@ public final class StoryItemSetContainerComponent: Component { self.sendMessageContext.animateOut(bounds: self.bounds) + self.sendMessageContext.tooltipScreen?.dismiss() + self.sendMessageContext.tooltipScreen = nil + + self.contextController?.dismiss() + self.contextController = nil + if let inputPanelView = self.inputPanel.view { inputPanelView.layer.animatePosition( from: CGPoint(), @@ -2508,7 +2514,7 @@ public final class StoryItemSetContainerComponent: Component { let tooltipText: String switch storyPrivacyIcon { case .closeFriends: - tooltipText = component.strings.Story_TooltipPrivacyCloseFriends(component.slice.peer.compactDisplayTitle).string + tooltipText = component.strings.Story_TooltipPrivacyCloseFriends2(component.slice.peer.compactDisplayTitle).string case .contacts: tooltipText = component.strings.Story_TooltipPrivacyContacts(component.slice.peer.compactDisplayTitle).string case .selectedContacts: @@ -2520,7 +2526,10 @@ public final class StoryItemSetContainerComponent: Component { let tooltipScreen = TooltipScreen( account: component.context.account, sharedContext: component.context.sharedContext, - text: .markdown(text: tooltipText), style: .default, location: TooltipScreen.Location.point(closeFriendIconView.convert(closeFriendIconView.bounds, to: nil).offsetBy(dx: 1.0, dy: 6.0), .top), displayDuration: .infinite, shouldDismissOnTouch: { _, _ in + text: .markdown(text: tooltipText), + balancedTextLayout: true, + style: .default, + location: TooltipScreen.Location.point(closeFriendIconView.convert(closeFriendIconView.bounds, to: nil).offsetBy(dx: 1.0, dy: 6.0), .top), displayDuration: .infinite, shouldDismissOnTouch: { _, _ in return .dismiss(consume: true) } ) @@ -3376,23 +3385,41 @@ public final class StoryItemSetContainerComponent: Component { break } - if subject != nil || chat { - component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: component.context, chatLocation: .peer(peer), subject: subject, keepStack: .always, animated: true, pushController: { [weak controller, weak navigationController] chatController, animated, completion in - guard let controller, let navigationController else { - return - } - if "".isEmpty { - navigationController.pushViewController(chatController) + if subject != nil || chat { + if let index = navigationController.viewControllers.firstIndex(where: { c in + if let c = c as? ChatController, case .peer(peer.id) = c.chatLocation { + return true } else { - var viewControllers = navigationController.viewControllers - if let index = viewControllers.firstIndex(where: { $0 === controller }) { - viewControllers.insert(chatController, at: index) - } else { - viewControllers.append(chatController) - } - navigationController.setViewControllers(viewControllers, animated: animated) + return false } - })) + }) { + var viewControllers = navigationController.viewControllers + for i in ((index + 1) ..< viewControllers.count).reversed() { + if viewControllers[i] !== controller { + viewControllers.remove(at: i) + } + } + navigationController.setViewControllers(viewControllers, animated: true) + + controller.dismissWithoutTransitionOut() + } else { + component.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: component.context, chatLocation: .peer(peer), subject: subject, keepStack: .always, animated: true, pushController: { [weak controller, weak navigationController] chatController, animated, completion in + guard let controller, let navigationController else { + return + } + if "".isEmpty { + navigationController.pushViewController(chatController) + } else { + var viewControllers = navigationController.viewControllers + if let index = viewControllers.firstIndex(where: { $0 === controller }) { + viewControllers.insert(chatController, at: index) + } else { + viewControllers.append(chatController) + } + navigationController.setViewControllers(viewControllers, animated: animated) + } + })) + } } else { var currentViewControllers = navigationController.viewControllers if let index = currentViewControllers.firstIndex(where: { c in @@ -3652,6 +3679,10 @@ public final class StoryItemSetContainerComponent: Component { } private func performMoreAction(sourceView: UIView, gesture: ContextGesture?) { + if self.isAnimatingOut { + return + } + guard let component = self.component else { return } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index 3cbf85aa86..82df7f8971 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -1684,11 +1684,11 @@ final class StoryItemSetContainerSendMessage { done(time) }) } - controller.getCaptionPanelView = { [weak self, weak view] in - guard let self, let view else { + controller.getCaptionPanelView = { [weak self, weak controller, weak view] in + guard let self, let view, let controller else { return nil } - return self.getCaptionPanelView(view: view, peer: peer) + return self.getCaptionPanelView(view: view, peer: peer, mediaPicker: controller) } controller.legacyCompletion = { signals, silently, scheduleTime, getAnimatedTransitionSource, sendCompletion in completion(signals, silently, scheduleTime, getAnimatedTransitionSource, sendCompletion) @@ -2067,7 +2067,7 @@ final class StoryItemSetContainerSendMessage { }) } - private func getCaptionPanelView(view: StoryItemSetContainerComponent.View, peer: EnginePeer) -> TGCaptionPanelView? { + private func getCaptionPanelView(view: StoryItemSetContainerComponent.View, peer: EnginePeer, mediaPicker: MediaPickerScreen? = nil) -> TGCaptionPanelView? { guard let component = view.component else { return nil } @@ -2081,7 +2081,27 @@ final class StoryItemSetContainerSendMessage { guard let view else { return } - view.component?.controller()?.presentInGlobalOverlay(c) + if let c = c as? PremiumIntroScreen { + view.endEditing(true) + if let mediaPicker { + mediaPicker.closeGalleryController() + } + if let attachmentController = self.attachmentController { + self.attachmentController = nil + attachmentController.dismiss(animated: false, completion: nil) + } + c.wasDismissed = { [weak view] in + guard let view else { + return + } + view.updateIsProgressPaused() + } + view.component?.controller()?.push(c) + + view.updateIsProgressPaused() + } else { + view.component?.controller()?.presentInGlobalOverlay(c) + } }) as? TGCaptionPanelView } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift index e7bee94391..ea9fed6c84 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetViewListComponent.swift @@ -831,7 +831,7 @@ final class StoryItemSetViewListComponent: Component { transition: emptyTransition, component: AnyComponent(AnimatedStickerComponent( account: component.context.account, - animation: AnimatedStickerComponent.Animation(source: .bundle(name: "Burn"), loop: true), + animation: AnimatedStickerComponent.Animation(source: .bundle(name: "ChatListNoResults"), loop: true), size: CGSize(width: 140.0, height: 140.0) )), environment: {}, diff --git a/submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/Contents.json index 5312a6642c..0d1e12bb58 100644 --- a/submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "smoothGradient 0.6.png", + "filename" : "smoothGradient 0.4.png", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/smoothGradient 0.4.png b/submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/smoothGradient 0.4.png new file mode 100644 index 0000000000000000000000000000000000000000..49a5faf1c2eba72750360854a0ff715644aab620 GIT binary patch literal 1353 zcmV-P1-AN$P)U>h*?d$b`aT5T$j!+ zmv){4Q-A$k)t||l8vWnYL5&#};E{=yZHo!sB_1(oDdMW&yvxUe(=MMoo|R;r_?B4G z3TlS9Ni3&3^n3@Gie;^}gV^4m)mN9r9O_ZbVG|}8icnF31O?%L{L_vm;#3HD6?Va!XbBie+=k*;rwF^@ErnnmGiG7&g~k7pMfj= z2Nx-7yi*22%K!iXIAvH#W=%~1DgXcg2mk?xX#fNO00031000^Q000000-yo_1ONa4 z0RR91AfN*P1ONa40RR91fB*mh0RL&G9u_c0$ik3K=J9al?b&DjK;yG@`m%Jsdc!ww`0foqz2SGw@V*$+(pcq} z_b3bn=np@oUIQ(7EuP+yqHErybl0V6|8N$7R5Ra0XH8I zwb|AN09k{0266~8fSk(l0H@qYYiimoGwtR$;+Gl7w=(D*E(ie(>=Vd?HRFdPbD+nf zISzp4pr7{Lz_D7vNK%Y14!UxbGJ_InJ zs!KB!tG?YKKR)og3a2#cdu*e;3lrdk(r+}!TsP-s~tm9;uz=9j{4AiOTdC% z)|RgVm8p7_sOx9vSOz-+5WWSIz*GPVgdW9PY{66@T!BeoDjF9HbyvUI z9(@m_U)~}W(0?~mw+LGhslfbPKNX1mP>mB5ehNaL15hBGz}$jAb>dMq1$ra>00000 LNkvXXu0mjfnkY@E literal 0 HcmV?d00001 diff --git a/submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/smoothGradient 0.6.png b/submodules/TelegramUI/Images.xcassets/Stories/PanelGradient.imageset/smoothGradient 0.6.png deleted file mode 100644 index 82f7e1820c53f4d7c4cf9c8fbca1a98feea87e47..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1781 zcmVU>h*?d$b`aT5T$j!+ zmv){4Q-A$k)t||l8vWnYL5&#};E{=yZHo!sB_1(oDdMW&yvxUe(=MMoo|R;r_?B4G z3TlS9Ni3&3^n3@Gie;^}gV^4m)mN9r9O_ZbVG|}8icnF31O?%L{L_vm;#3HD6?Va!XbBie+=k*;rwF^@ErnnmGiG7&g~k7pMfj= z2Nx-7yi*22%K!iXIAvH#W=%~1DgXcg2mk?xX#fNO00031000^Q000000-yo_1ONa4 z0RR91AfN*P1ONa40RR91fB*mh0RL&8L#}MahzFx|T6SKuU@tkB%r>cAczNs;ylgpSCql!>4V{&CSi% zw&#@Q;o)H`4TwKU1LF7EaL|U|+VDpkqL7{T_^Ay6G4KefI|CPYc6zXK4+TqLg*!vM z$9(xfg2?3z4pGLY$_H}cTeN%*P=dx4J1Nb8Mfl#c6BEk#~IHY;;B5`PKf0|Xu zJOG;0__=zey0}@zE34=L9-R$?*`n>`kiGnXfW+X#{5$odPX`(y*@BGHI9PJ}!GR2r z6lohI0g%m72`vS{j8Xz4rF&a*JRSiux;0NvPuWr_Ap;;=twn&Bmlqqr>+5UwiYqd3 zXS@x-%^**W(uC1Cw$KcaJ$<7ol>rQv4Ir-pMc#MU%U?o@c3_*N4$OsQz81^?$OHC! zR70@sKyd)JGtmKng>2E2gdB9b9QP0Sliyg>g!!&%2LNrNpXWB$C@OGt6@sKW2W9~C zHvWeI@@}BZ>^1gN?xy0Q%loVjYYl*bHFKftkSt zV1jMDB=IeYIGN;I7+x^EXK;*utwaKspF~NYOKYWd3I1XG)6N6=Sl?0>iXX=k%nU97 z#s}_6cER?9;_9G201pjOrTtYT(YypOXpbnT%Zo^#1#-;Olnl5ON;pV8)%WM2cJj!5 z|A2u7z@JDKfVF7{uuSU`Y&8G|<|P26YQqlV6IJLtD^@bPa<`?w)ZfFrkg=WdBp?H4 z)vNCz2{64fBh)#j(oVpB_pd8Q1 zQfxa80{C!(6w}m1@Rz=0;(_bGqpn(nxh+Vod~zGNU~=HTfR_v`Vhcc`TDxRmaT(wl zJ+>wW$1g4eV^=;p4jD-OP^NTuxGEfYh%3%=%>b@UvsjN;Qbrnpt#YG&{K=lk<2p|LUl*7L9q@z7Jz2M*) zZEwA|zqC=(50VF0AW;Kw#wXRJojN7_rOtTTD7)-_ym}E Signal { if let representation = representation as? CachedStickerAJpegRepresentation { @@ -38,7 +39,33 @@ public func fetchCachedResourceRepresentation(account: Account, resource: MediaR return fetchCachedScaledImageRepresentation(resource: resource, resourceData: data, representation: representation) } } else if let _ = representation as? CachedVideoFirstFrameRepresentation { - return account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) + return Signal { subscriber in + if let size = resource.size { + let videoSource = UniversalSoftwareVideoSource(mediaBox: account.postbox.mediaBox, source: .direct(resource: resource, size: size), automaticallyFetchHeader: false, hintVP9: false) + let disposable = videoSource.takeFrame(at: 0.0).start(next: { value in + switch value { + case let .image(image): + if let image { + if let imageData = image.jpegData(compressionQuality: 0.6) { + subscriber.putNext(.data(imageData)) + subscriber.putNext(.done) + subscriber.putCompletion() + } + } + case .waitingForData: + break + } + }) + return ActionDisposable { + // keep the reference + let _ = videoSource.takeFrame(at: 0.0) + disposable.dispose() + } + } else { + return EmptyDisposable + } + } + /*return account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) |> mapToSignal { data -> Signal in if data.complete { return fetchCachedVideoFirstFrameRepresentation(account: account, resource: resource, resourceData: data) @@ -50,7 +77,7 @@ public func fetchCachedResourceRepresentation(account: Account, resource: MediaR } else { return .complete() } - } + }*/ } else if let representation = representation as? CachedScaledVideoFirstFrameRepresentation { return account.postbox.mediaBox.resourceData(resource, option: .complete(waitUntilFetchStatus: false)) |> mapToSignal { data -> Signal in diff --git a/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift b/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift index 949e624040..0f606a494e 100644 --- a/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift +++ b/submodules/TelegramUI/Sources/PeerInfoGifPaneNode.swift @@ -65,9 +65,11 @@ private final class FrameSequenceThumbnailNode: ASDisplayNode { let source = UniversalSoftwareVideoSource( mediaBox: self.context.account.postbox.mediaBox, - userLocation: userLocation, - userContentType: .other, - fileReference: self.file, + source: .file( + userLocation: userLocation, + userContentType: .other, + fileReference: self.file + ), automaticallyFetchHeader: true ) self.sources.append(source) diff --git a/submodules/TooltipUI/BUILD b/submodules/TooltipUI/BUILD index 1d49fcf788..6ef6c4ed14 100644 --- a/submodules/TooltipUI/BUILD +++ b/submodules/TooltipUI/BUILD @@ -24,6 +24,7 @@ swift_library( "//submodules/ComponentFlow", "//submodules/Markdown", "//submodules/TelegramUI/Components/Stories/AvatarStoryIndicatorComponent", + "//submodules/Components/BalancedTextComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TooltipUI/Sources/TooltipScreen.swift b/submodules/TooltipUI/Sources/TooltipScreen.swift index 4f2b673a16..ba2501f419 100644 --- a/submodules/TooltipUI/Sources/TooltipScreen.swift +++ b/submodules/TooltipUI/Sources/TooltipScreen.swift @@ -16,6 +16,7 @@ import ComponentFlow import AvatarStoryIndicatorComponent import AccountContext import Markdown +import BalancedTextComponent public enum TooltipActiveTextItem { case url(String, Bool) @@ -107,6 +108,9 @@ private class DownArrowsIconNode: ASDisplayNode { } private final class TooltipScreenNode: ViewControllerTracingNode { + private let text: TooltipScreen.Text + private let textAlignment: TooltipScreen.Alignment + private let balancedTextLayout: Bool private let tooltipStyle: TooltipScreen.Style private let icon: TooltipScreen.Icon? private let action: TooltipScreen.Action? @@ -136,12 +140,13 @@ private final class TooltipScreenNode: ViewControllerTracingNode { private var downArrowsNode: DownArrowsIconNode? private var avatarNode: AvatarNode? private var avatarStoryIndicator: ComponentView? - private let textNode: ImmediateTextNode + private let textView = ComponentView() private var closeButtonNode: HighlightableButtonNode? private var actionButtonNode: HighlightableButtonNode? private var isArrowInverted: Bool = false + private let fontSize: CGFloat private let inset: CGFloat private var validLayout: ContainerViewLayout? @@ -152,6 +157,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { sharedContext: SharedAccountContext, text: TooltipScreen.Text, textAlignment: TooltipScreen.Alignment, + balancedTextLayout: Bool, style: TooltipScreen.Style, icon: TooltipScreen.Icon? = nil, action: TooltipScreen.Action? = nil, @@ -337,39 +343,10 @@ private final class TooltipScreenNode: ViewControllerTracingNode { self.backgroundMaskNode.layer.rasterizationScale = UIScreen.main.scale } - self.textNode = ImmediateTextNode() - self.textNode.displaysAsynchronously = false - self.textNode.maximumNumberOfLines = 0 - - let baseFont = Font.regular(fontSize) - let boldFont = Font.semibold(14.0) - let italicFont = Font.italic(fontSize) - let boldItalicFont = Font.semiboldItalic(fontSize) - let fixedFont = Font.monospace(fontSize) - - let textColor: UIColor = .white - - let attributedText: NSAttributedString - switch text { - case let .plain(text): - attributedText = NSAttributedString(string: text, font: baseFont, textColor: textColor) - case let .entities(text, entities): - attributedText = stringWithAppliedEntities(text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: baseFont, linkFont: baseFont, boldFont: boldFont, italicFont: italicFont, boldItalicFont: boldItalicFont, fixedFont: fixedFont, blockQuoteFont: baseFont, underlineLinks: true, external: false, message: nil) - case let .markdown(text): - let linkColor = UIColor(rgb: 0x64d2ff) - let markdownAttributes = MarkdownAttributes( - body: MarkdownAttributeSet(font: baseFont, textColor: textColor), - bold: MarkdownAttributeSet(font: boldFont, textColor: textColor), - link: MarkdownAttributeSet(font: boldFont, textColor: linkColor), - linkAttribute: { _ in - return nil - } - ) - attributedText = parseMarkdownIntoAttributedString(text, attributes: markdownAttributes) - } - - self.textNode.attributedText = attributedText - self.textNode.textAlignment = textAlignment == .center ? .center : .natural + self.fontSize = fontSize + self.text = text + self.textAlignment = textAlignment + self.balancedTextLayout = balancedTextLayout self.animatedStickerNode = DefaultAnimatedStickerNodeImpl() switch icon { @@ -403,7 +380,6 @@ private final class TooltipScreenNode: ViewControllerTracingNode { self.backgroundContainerNode.addSubnode(effectNode) self.backgroundContainerNode.layer.mask = self.backgroundMaskNode.layer } - self.containerNode.addSubnode(self.textNode) self.containerNode.addSubnode(self.animatedStickerNode) if let closeButtonNode = self.closeButtonNode { @@ -428,65 +404,6 @@ private final class TooltipScreenNode: ViewControllerTracingNode { self.actionButtonNode = actionButtonNode } - self.textNode.linkHighlightColor = UIColor.white.withAlphaComponent(0.5) - self.textNode.highlightAttributeAction = { attributes in - let highlightedAttributes = [ - TelegramTextAttributes.URL, - TelegramTextAttributes.PeerMention, - TelegramTextAttributes.PeerTextMention, - TelegramTextAttributes.BotCommand, - TelegramTextAttributes.Hashtag - ] - - for attribute in highlightedAttributes { - if let _ = attributes[NSAttributedString.Key(rawValue: attribute)] { - return NSAttributedString.Key(rawValue: attribute) - } - } - return nil - } - self.textNode.tapAttributeAction = { [weak self] attributes, index in - guard let strongSelf = self else { - return - } - if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { - var concealed = true - if let (attributeText, fullText) = strongSelf.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { - concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) - } - openActiveTextItem?(.url(url, concealed), .tap) - } else if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { - openActiveTextItem?(.mention(mention.peerId, mention.mention), .tap) - } else if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { - openActiveTextItem?(.textMention(mention), .tap) - } else if let command = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { - openActiveTextItem?(.botCommand(command), .tap) - } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { - openActiveTextItem?(.hashtag(hashtag.hashtag), .tap) - } - } - - self.textNode.longTapAttributeAction = { [weak self] attributes, index in - guard let strongSelf = self else { - return - } - if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { - var concealed = true - if let (attributeText, fullText) = strongSelf.textNode.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { - concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) - } - openActiveTextItem?(.url(url, concealed), .longTap) - } else if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { - openActiveTextItem?(.mention(mention.peerId, mention.mention), .longTap) - } else if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { - openActiveTextItem?(.textMention(mention), .longTap) - } else if let command = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { - openActiveTextItem?(.botCommand(command), .longTap) - } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { - openActiveTextItem?(.hashtag(hashtag.hashtag), .longTap) - } - } - self.actionButtonNode?.addTarget(self, action: #selector(self.actionPressed), forControlEvents: .touchUpInside) self.closeButtonNode?.addTarget(self, action: #selector(self.closePressed), forControlEvents: .touchUpInside) } @@ -555,7 +472,105 @@ private final class TooltipScreenNode: ViewControllerTracingNode { buttonInset += 24.0 } - let textSize = self.textNode.updateLayout(CGSize(width: containerWidth - contentInset * 2.0 - animationSize.width - animationSpacing - buttonInset, height: .greatestFiniteMagnitude)) + let baseFont = Font.regular(self.fontSize) + let boldFont = Font.semibold(14.0) + let italicFont = Font.italic(self.fontSize) + let boldItalicFont = Font.semiboldItalic(self.fontSize) + let fixedFont = Font.monospace(self.fontSize) + + let textColor: UIColor = .white + let attributedText: NSAttributedString + switch self.text { + case let .plain(text): + attributedText = NSAttributedString(string: text, font: baseFont, textColor: textColor) + case let .entities(text, entities): + attributedText = stringWithAppliedEntities(text, entities: entities, baseColor: textColor, linkColor: textColor, baseFont: baseFont, linkFont: baseFont, boldFont: boldFont, italicFont: italicFont, boldItalicFont: boldItalicFont, fixedFont: fixedFont, blockQuoteFont: baseFont, underlineLinks: true, external: false, message: nil) + case let .markdown(text): + let linkColor = UIColor(rgb: 0x64d2ff) + let markdownAttributes = MarkdownAttributes( + body: MarkdownAttributeSet(font: baseFont, textColor: textColor), + bold: MarkdownAttributeSet(font: boldFont, textColor: textColor), + link: MarkdownAttributeSet(font: boldFont, textColor: linkColor), + linkAttribute: { _ in + return nil + } + ) + attributedText = parseMarkdownIntoAttributedString(text, attributes: markdownAttributes) + } + + let highlightColor: UIColor? = UIColor.white.withAlphaComponent(0.5) + let highlightAction: (([NSAttributedString.Key: Any]) -> NSAttributedString.Key?)? = { attributes in + let highlightedAttributes = [ + TelegramTextAttributes.URL, + TelegramTextAttributes.PeerMention, + TelegramTextAttributes.PeerTextMention, + TelegramTextAttributes.BotCommand, + TelegramTextAttributes.Hashtag + ] + + for attribute in highlightedAttributes { + if let _ = attributes[NSAttributedString.Key(rawValue: attribute)] { + return NSAttributedString.Key(rawValue: attribute) + } + } + return nil + } + let tapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = { [weak self] attributes, index in + guard let strongSelf = self else { + return + } + if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + var concealed = true + if let (attributeText, fullText) = (strongSelf.textView.view as? BalancedTextComponent.View)?.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { + concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) + } + strongSelf.openActiveTextItem?(.url(url, concealed), .tap) + } else if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { + strongSelf.openActiveTextItem?(.mention(mention.peerId, mention.mention), .tap) + } else if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { + strongSelf.openActiveTextItem?(.textMention(mention), .tap) + } else if let command = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { + strongSelf.openActiveTextItem?(.botCommand(command), .tap) + } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { + strongSelf.openActiveTextItem?(.hashtag(hashtag.hashtag), .tap) + } + } + let longTapAction: (([NSAttributedString.Key: Any], Int) -> Void)? = { [weak self] attributes, index in + guard let strongSelf = self else { + return + } + if let url = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.URL)] as? String { + var concealed = true + if let (attributeText, fullText) = (strongSelf.textView.view as? BalancedTextComponent.View)?.attributeSubstring(name: TelegramTextAttributes.URL, index: index) { + concealed = !doesUrlMatchText(url: url, text: attributeText, fullText: fullText) + } + strongSelf.openActiveTextItem?(.url(url, concealed), .longTap) + } else if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerMention)] as? TelegramPeerMention { + strongSelf.openActiveTextItem?(.mention(mention.peerId, mention.mention), .longTap) + } else if let mention = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.PeerTextMention)] as? String { + strongSelf.openActiveTextItem?(.textMention(mention), .longTap) + } else if let command = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.BotCommand)] as? String { + strongSelf.openActiveTextItem?(.botCommand(command), .longTap) + } else if let hashtag = attributes[NSAttributedString.Key(rawValue: TelegramTextAttributes.Hashtag)] as? TelegramHashtag { + strongSelf.openActiveTextItem?(.hashtag(hashtag.hashtag), .longTap) + } + } + + let textSize = self.textView.update( + transition: .immediate, + component: AnyComponent(BalancedTextComponent( + text: .plain(attributedText), + balanced: self.balancedTextLayout, + horizontalAlignment: self.textAlignment == .center ? .center : .left, + maximumNumberOfLines: 0, + highlightColor: highlightColor, + highlightAction: highlightAction, + tapAction: tapAction, + longTapAction: longTapAction + )), + environment: {}, + containerSize: CGSize(width: containerWidth - contentInset * 2.0 - animationSize.width - animationSpacing - buttonInset, height: 1000000.0) + ) var backgroundFrame: CGRect @@ -668,7 +683,15 @@ private final class TooltipScreenNode: ViewControllerTracingNode { } let textFrame = CGRect(origin: CGPoint(x: contentInset + animationSize.width + animationSpacing, y: floor((backgroundHeight - textSize.height) / 2.0)), size: textSize) - transition.updateFrame(node: self.textNode, frame: textFrame) + + if let textComponentView = self.textView.view { + if textComponentView.superview == nil { + textComponentView.layer.anchorPoint = CGPoint() + self.containerNode.view.addSubview(textComponentView) + } + transition.updatePosition(layer: textComponentView.layer, position: textFrame.origin) + transition.updateBounds(layer: textComponentView.layer, bounds: CGRect(origin: CGPoint(), size: textFrame.size)) + } if let closeButtonNode = self.closeButtonNode { let closeSize = CGSize(width: 44.0, height: 44.0) @@ -746,7 +769,7 @@ private final class TooltipScreenNode: ViewControllerTracingNode { private var didRequestDismiss = false override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { if let event = event { - if let _ = self.openActiveTextItem, let result = self.textNode.hitTest(self.view.convert(point, to: self.textNode.view), with: event) { + if let _ = self.openActiveTextItem, let textComponentView = self.textView.view, let result = textComponentView.hitTest(self.view.convert(point, to: textComponentView), with: event) { return result } @@ -940,6 +963,7 @@ public final class TooltipScreen: ViewController { private let sharedContext: SharedAccountContext public let text: TooltipScreen.Text public let textAlignment: TooltipScreen.Alignment + private let balancedTextLayout: Bool private let style: TooltipScreen.Style private let icon: TooltipScreen.Icon? private let action: TooltipScreen.Action? @@ -976,6 +1000,7 @@ public final class TooltipScreen: ViewController { sharedContext: SharedAccountContext, text: TooltipScreen.Text, textAlignment: TooltipScreen.Alignment = .natural, + balancedTextLayout: Bool = false, style: TooltipScreen.Style = .default, icon: TooltipScreen.Icon? = nil, action: TooltipScreen.Action? = nil, @@ -991,6 +1016,7 @@ public final class TooltipScreen: ViewController { self.sharedContext = sharedContext self.text = text self.textAlignment = textAlignment + self.balancedTextLayout = balancedTextLayout self.style = style self.icon = icon self.action = action @@ -1057,7 +1083,7 @@ public final class TooltipScreen: ViewController { } override public func loadDisplayNode() { - self.displayNode = TooltipScreenNode(context: self.context, account: self.account, sharedContext: self.sharedContext, text: self.text, textAlignment: self.textAlignment, style: self.style, icon: self.icon, action: self.action, location: self.location, displayDuration: self.displayDuration, inset: self.inset, cornerRadius: self.cornerRadius, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in + self.displayNode = TooltipScreenNode(context: self.context, account: self.account, sharedContext: self.sharedContext, text: self.text, textAlignment: self.textAlignment, balancedTextLayout: self.balancedTextLayout, style: self.style, icon: self.icon, action: self.action, location: self.location, displayDuration: self.displayDuration, inset: self.inset, cornerRadius: self.cornerRadius, shouldDismissOnTouch: self.shouldDismissOnTouch, requestDismiss: { [weak self] in guard let strongSelf = self else { return } From d3dadc0a9923a9435978a9375abe4e26ed8ddfe6 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Wed, 26 Jul 2023 20:23:59 +0400 Subject: [PATCH 5/6] Stories (cherry picked from commit c5039d9be1dfef2a8c9215ed1aa3d2e2b128f630) --- .../Sources/ChatListControllerNode.swift | 10 +- .../Messages/StoryListContext.swift | 16 ++- .../Sources/PeerInfoStoryGridScreen.swift | 107 +++++++++++++++--- .../Sources/PeerInfoStoryPaneNode.swift | 39 ++++--- .../Sources/PeerInfo/PeerInfoData.swift | 34 ++++-- .../Sources/PeerInfo/PeerInfoScreen.swift | 8 +- 6 files changed, 155 insertions(+), 59 deletions(-) diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index d763c76347..276f74b913 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -2018,10 +2018,14 @@ final class ChatListControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { } var effectiveStorySubscriptions: EngineStorySubscriptions? - if let controller = self.controller, let storySubscriptions = controller.orderedStorySubscriptions, shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions, isHidden: controller.location == .chatList(groupId: .archive)) { - effectiveStorySubscriptions = controller.orderedStorySubscriptions + if let controller = self.controller, case .forum = controller.location { + effectiveStorySubscriptions = nil } else { - effectiveStorySubscriptions = EngineStorySubscriptions(accountItem: nil, items: [], hasMoreToken: nil) + if let controller = self.controller, let storySubscriptions = controller.orderedStorySubscriptions, shouldDisplayStoriesInChatListHeader(storySubscriptions: storySubscriptions, isHidden: controller.location == .chatList(groupId: .archive)) { + effectiveStorySubscriptions = controller.orderedStorySubscriptions + } else { + effectiveStorySubscriptions = EngineStorySubscriptions(accountItem: nil, items: [], hasMoreToken: nil) + } } let navigationBarSize = self.navigationBarView.update( diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift index cb5ae41885..90d42e158b 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/StoryListContext.swift @@ -470,15 +470,15 @@ public final class PeerStoryListContext { self.peerId = peerId self.isArchived = isArchived - self.stateValue = State(peerReference: nil, items: [], totalCount: 0, loadMoreToken: 0, isCached: true, allEntityFiles: [:]) + self.stateValue = State(peerReference: nil, items: [], totalCount: 0, loadMoreToken: 0, isCached: true, hasCache: false, allEntityFiles: [:]) - let _ = (account.postbox.transaction { transaction -> (PeerReference?, [EngineStoryItem], Int, [MediaId: TelegramMediaFile]) in + let _ = (account.postbox.transaction { transaction -> (PeerReference?, [EngineStoryItem], Int, [MediaId: TelegramMediaFile], Bool) in let key = ValueBoxKey(length: 8 + 1) key.setInt64(0, value: peerId.toInt64()) key.setInt8(8, value: isArchived ? 1 : 0) let cached = transaction.retrieveItemCacheEntry(id: ItemCacheEntryId(collectionId: Namespaces.CachedItemCollection.cachedPeerStoryListHeads, key: key))?.get(CachedPeerStoryListHead.self) guard let cached = cached else { - return (nil, [], 0, [:]) + return (nil, [], 0, [:], false) } var items: [EngineStoryItem] = [] var allEntityFiles: [MediaId: TelegramMediaFile] = [:] @@ -527,14 +527,14 @@ public final class PeerStoryListContext { let peerReference = transaction.getPeer(peerId).flatMap(PeerReference.init) - return (peerReference, items, Int(cached.totalCount), allEntityFiles) + return (peerReference, items, Int(cached.totalCount), allEntityFiles, true) } - |> deliverOn(self.queue)).start(next: { [weak self] peerReference, items, totalCount, allEntityFiles in + |> deliverOn(self.queue)).start(next: { [weak self] peerReference, items, totalCount, allEntityFiles, hasCache in guard let `self` = self else { return } - self.stateValue = State(peerReference: peerReference, items: items, totalCount: totalCount, loadMoreToken: 0, isCached: true, allEntityFiles: allEntityFiles) + self.stateValue = State(peerReference: peerReference, items: items, totalCount: totalCount, loadMoreToken: 0, isCached: true, hasCache: hasCache, allEntityFiles: allEntityFiles) self.loadMore(completion: nil) }) } @@ -665,6 +665,7 @@ public final class PeerStoryListContext { updatedState.items.removeAll() updatedState.isCached = false } + updatedState.hasCache = true var existingIds = Set(updatedState.items.map { $0.id }) for item in storyItems { @@ -939,6 +940,7 @@ public final class PeerStoryListContext { public var totalCount: Int public var loadMoreToken: Int? public var isCached: Bool + public var hasCache: Bool public var allEntityFiles: [MediaId: TelegramMediaFile] init( @@ -947,6 +949,7 @@ public final class PeerStoryListContext { totalCount: Int, loadMoreToken: Int?, isCached: Bool, + hasCache: Bool, allEntityFiles: [MediaId: TelegramMediaFile] ) { self.peerReference = peerReference @@ -954,6 +957,7 @@ public final class PeerStoryListContext { self.totalCount = totalCount self.loadMoreToken = loadMoreToken self.isCached = isCached + self.hasCache = hasCache self.allEntityFiles = allEntityFiles } } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift index bc21449495..304cf81128 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoStoryGridScreen/Sources/PeerInfoStoryGridScreen.swift @@ -50,7 +50,7 @@ final class PeerInfoStoryGridScreenComponent: Component { final class View: UIView { private var component: PeerInfoStoryGridScreenComponent? - private weak var state: EmptyComponentState? + private(set) weak var state: EmptyComponentState? private var environment: EnvironmentType? private(set) var paneNode: PeerInfoStoryPaneNode? @@ -172,6 +172,24 @@ final class PeerInfoStoryGridScreenComponent: Component { }))) } } + + if let paneNode = self.paneNode, !paneNode.isSelectionModeActive, case .saved = component.scope { + if !paneNode.isEmpty { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ContextMenuSelect, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self, let paneNode = self.paneNode else { + return + } + + paneNode.setIsSelectionModeActive(true) + + (self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle() + }))) + } + } let contextController = ContextController(account: component.context.account, presentationData: presentationData, source: .reference(PeerInfoContextReferenceContentSource(controller: controller, sourceNode: source)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) contextController.passthroughTouchEvent = { [weak self] sourceView, point in @@ -306,11 +324,19 @@ final class PeerInfoStoryGridScreenComponent: Component { self.selectionPanel = selectionPanel } + let buttonText: String + switch component.scope { + case .saved: + buttonText = environment.strings.Common_Delete + case .archive: + buttonText = environment.strings.StoryList_SaveToProfile + } + let selectionPanelSize = selectionPanel.update( transition: selectionPanelTransition, component: AnyComponent(BottomButtonPanelComponent( theme: environment.theme, - title: environment.strings.StoryList_SaveToProfile, + title: buttonText, label: nil, isEnabled: true, insets: UIEdgeInsets(top: 0.0, left: sideInset, bottom: environment.safeInsets.bottom, right: sideInset), @@ -322,20 +348,49 @@ final class PeerInfoStoryGridScreenComponent: Component { return } - let _ = component.context.engine.messages.updateStoriesArePinned(ids: paneNode.selectedItems, isPinned: true).start() - - let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) - - let title: String = presentationData.strings.StoryList_TooltipStoriesSavedToProfile(Int32(paneNode.selectedIds.count)) - environment.controller()?.present(UndoOverlayController( - presentationData: presentationData, - content: .info(title: title, text: presentationData.strings.StoryList_TooltipStoriesSavedToProfileText, timeout: nil), - elevatedLayout: false, - animateInAsReplacement: false, - action: { _ in return false } - ), in: .current) - - paneNode.clearSelection() + switch component.scope { + case .saved: + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) + let actionSheet = ActionSheetController(presentationData: presentationData) + + actionSheet.setItemGroups([ + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Delete, color: .destructive, action: { [weak self, weak actionSheet] in + actionSheet?.dismissAnimated() + + guard let self, let paneNode = self.paneNode, let component = self.component else { + return + } + let _ = component.context.engine.messages.deleteStories(ids: Array(paneNode.selectedIds)).start() + + paneNode.setIsSelectionModeActive(false) + (self.environment?.controller() as? PeerInfoStoryGridScreen)?.updateTitle() + }) + ]), + ActionSheetItemGroup(items: [ + ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in + actionSheet?.dismissAnimated() + }) + ]) + ]) + + self.environment?.controller()?.present(actionSheet, in: .window(.root)) + case .archive: + let _ = component.context.engine.messages.updateStoriesArePinned(ids: paneNode.selectedItems, isPinned: true).start() + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: environment.theme) + + let title: String = presentationData.strings.StoryList_TooltipStoriesSavedToProfile(Int32(paneNode.selectedIds.count)) + environment.controller()?.present(UndoOverlayController( + presentationData: presentationData, + content: .info(title: title, text: presentationData.strings.StoryList_TooltipStoriesSavedToProfileText, timeout: nil), + elevatedLayout: false, + animateInAsReplacement: false, + action: { _ in return false } + ), in: .current) + + paneNode.clearSelection() + } } )), environment: {}, @@ -462,6 +517,7 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer { private var moreBarButton: MoreHeaderButton? private var moreBarButtonItem: UIBarButtonItem? + private var doneBarButtonItem: UIBarButtonItem? public init( context: AccountContext, @@ -493,6 +549,9 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer { } moreBarButton.addTarget(self, action: #selector(self.morePressed), forControlEvents: .touchUpInside) + let doneBarButtonItem = UIBarButtonItem(title: presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)) + self.doneBarButtonItem = doneBarButtonItem + self.titleView = ChatTitleView( context: context, theme: presentationData.theme, @@ -528,7 +587,7 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer { switch self.scope { case .saved: - guard let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View else { + guard let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View, let paneNode = componentView.paneNode else { return } let title: String? @@ -539,7 +598,11 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer { } self.titleView?.titleContent = .custom(presentationData.strings.StoryList_TitleSaved, title, false) - self.navigationItem.setRightBarButton(self.moreBarButtonItem, animated: false) + if paneNode.isSelectionModeActive { + self.navigationItem.setRightBarButton(self.doneBarButtonItem, animated: false) + } else { + self.navigationItem.setRightBarButton(self.moreBarButtonItem, animated: false) + } case .archive: guard let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View else { return @@ -577,6 +640,14 @@ public class PeerInfoStoryGridScreen: ViewControllerComponentContainer { componentView.morePressed(source: moreBarButton.referenceNode) } + @objc private func donePressed() { + guard let componentView = self.node.hostView.componentView as? PeerInfoStoryGridScreenComponent.View, let paneNode = componentView.paneNode else { + return + } + paneNode.setIsSelectionModeActive(false) + self.updateTitle() + } + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index f14ca3c33f..18bd108744 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -874,6 +874,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } } + public private(set) var isSelectionModeActive: Bool + private var currentParams: (size: CGSize, topInset: CGFloat, sideInset: CGFloat, bottomInset: CGFloat, visibleHeight: CGFloat, isScrollingLockedAtTop: Bool, expandProgress: CGFloat, presentationData: PresentationData)? private let ready = Promise() @@ -930,6 +932,8 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr self.navigationController = navigationController self.isSaved = isSaved self.isArchive = isArchive + + self.isSelectionModeActive = isArchive self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } @@ -1228,7 +1232,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } ) //TODO:selection - if isArchive { + if isArchive || self.isSelectionModeActive { self._itemInteraction?.selectedIds = Set() } self.itemGridBinding.itemInteraction = self._itemInteraction @@ -1520,22 +1524,6 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr } public func updateContentType(contentType: ContentType) { - /*if self.contentType == contentType { - return - } - self.contentType = contentType - self.contentTypePromise.set(contentType) - - self.itemGrid.hideScrollingArea() - - var threadId: Int64? - if case let .replyThread(message) = chatLocation { - threadId = Int64(message.messageId.id) - } - - self.listSource = self.context.engine.messages.sparseMessageList(peerId: self.peerId, threadId: threadId, tag: tagMaskForType(self.contentType)) - self.isRequestingView = false - self.requestHistoryAroundVisiblePosition(synchronous: true, reloadAtTop: true)*/ } public func updateZoomLevel(level: ZoomLevel) { @@ -1544,6 +1532,23 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScr //let _ = updateVisualMediaStoredState(engine: self.context.engine, peerId: self.peerId, messageTag: self.stateTag, state: VisualMediaStoredState(zoomLevel: level.rawValue)).start() } + public func setIsSelectionModeActive(_ value: Bool) { + if self.isSelectionModeActive != value { + self.isSelectionModeActive = value + + if value { + if self._itemInteraction?.selectedIds == nil { + self._itemInteraction?.selectedIds = Set() + } + } else { + self._itemInteraction?.selectedIds = nil + } + + self.selectedIdsPromise.set(self._itemInteraction?.selectedIds ?? Set()) + self.updateSelectedItems(animated: true) + } + } + public func ensureMessageIsVisible(id: MessageId) { } diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift index 6a67c1f2d5..d81fe3fa4c 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoData.swift @@ -477,8 +477,11 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, |> distinctUntilChanged let storyListContext = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: false) - let hasStories: Signal = storyListContext.state - |> map { state -> Bool in + let hasStories: Signal = storyListContext.state + |> map { state -> Bool? in + if !state.hasCache { + return nil + } return !state.items.isEmpty } |> distinctUntilChanged @@ -564,7 +567,7 @@ func peerInfoScreenSettingsData(context: AccountContext, peerId: EnginePeer.Id, groupsInCommon: nil, linkedDiscussionPeer: nil, members: nil, - storyListContext: storyListContext, + storyListContext: hasStories == true ? storyListContext : nil, encryptionKeyFingerprint: nil, globalSettings: globalSettings, invitations: nil, @@ -705,8 +708,11 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen } let storyListContext = PeerStoryListContext(account: context.account, peerId: peerId, isArchived: false) - let hasStories: Signal = storyListContext.state - |> map { state -> Bool in + let hasStories: Signal = storyListContext.state + |> map { state -> Bool? in + if !state.hasCache { + return nil + } return !state.items.isEmpty } |> distinctUntilChanged @@ -722,14 +728,18 @@ func peerInfoScreenData(context: AccountContext, peerId: PeerId, strings: Presen |> map { peerView, availablePanes, globalNotificationSettings, encryptionKeyFingerprint, status, hasStories -> PeerInfoScreenData in var availablePanes = availablePanes - if hasStories, peerView.peers[peerView.peerId] is TelegramUser, peerView.peerId != context.account.peerId { - availablePanes?.insert(.stories, at: 0) - } - - if availablePanes != nil, groupsInCommon != nil, let cachedData = peerView.cachedData as? CachedUserData { - if cachedData.commonGroupCount != 0 { - availablePanes?.append(.groupsInCommon) + if let hasStories { + if hasStories, peerView.peers[peerView.peerId] is TelegramUser, peerView.peerId != context.account.peerId { + availablePanes?.insert(.stories, at: 0) } + + if availablePanes != nil, groupsInCommon != nil, let cachedData = peerView.cachedData as? CachedUserData { + if cachedData.commonGroupCount != 0 { + availablePanes?.append(.groupsInCommon) + } + } + } else { + availablePanes = nil } return PeerInfoScreenData( diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 919793691f..3defa801b2 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -792,9 +792,11 @@ private func settingsItems(data: PeerInfoScreenData?, context: AccountContext, p } } - items[.stories]!.append(PeerInfoScreenDisclosureItem(id: 0, text: presentationData.strings.Settings_MyStories, icon: PresentationResourcesSettings.stories, action: { - interaction.openSettings(.stories) - })) + if data.storyListContext != nil || data.peer?.isPremium == true { + items[.stories]!.append(PeerInfoScreenDisclosureItem(id: 0, text: presentationData.strings.Settings_MyStories, icon: PresentationResourcesSettings.stories, action: { + interaction.openSettings(.stories) + })) + } items[.shortcuts]!.append(PeerInfoScreenDisclosureItem(id: 1, text: presentationData.strings.Settings_SavedMessages, icon: PresentationResourcesSettings.savedMessages, action: { interaction.openSettings(.savedMessages) From f2070fb70a9e3e531d623dd01c2399679a70d726 Mon Sep 17 00:00:00 2001 From: Ali <> Date: Wed, 26 Jul 2023 23:31:03 +0400 Subject: [PATCH 6/6] Add story reply transition view (cherry picked from commit 4a0e81c712a47f27630d444ea0027e6d6d9cf6c0) --- .../TelegramUI/Sources/ChatController.swift | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index ac285db1e5..dac473a2ac 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -4585,13 +4585,32 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } if let result = itemNode.targetForStoryTransition(id: storyId) { + result.isHidden = true transitionOut = StoryContainerScreen.TransitionOut( destinationView: result, - transitionView: nil, + transitionView: StoryContainerScreen.TransitionView( + makeView: { [weak result] in + let parentView = UIView() + if let copyView = result?.snapshotContentTree(unhide: true) { + parentView.addSubview(copyView) + } + return parentView + }, + updateView: { copyView, state, transition in + guard let view = copyView.subviews.first else { + return + } + let size = state.sourceSize.interpolate(to: state.destinationSize, amount: state.progress) + transition.setPosition(view: view, position: CGPoint(x: size.width * 0.5, y: size.height * 0.5)) + transition.setScale(view: view, scale: size.width / state.destinationSize.width) + }, + insertCloneTransitionView: nil + ), destinationRect: result.bounds, destinationCornerRadius: 2.0, destinationIsAvatar: false, - completed: { + completed: { [weak result] in + result?.isHidden = false } ) }