diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 8c21c05b03..f57cd9b3f5 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -939,6 +939,7 @@ public enum PremiumIntroSource { case voiceToText case fasterDownload case translation + case stories } public enum PremiumDemoSubject { @@ -956,6 +957,7 @@ public enum PremiumDemoSubject { case animatedEmoji case emojiStatus case translation + case stories } public enum PremiumLimitSubject { diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index 5596929159..c983418d3c 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -38,14 +38,16 @@ final class CameraDeviceContext { private weak var previewView: CameraSimplePreviewView? private let exclusive: Bool + private let additional: Bool let device = CameraDevice() let input = CameraInput() let output: CameraOutput - init(session: CameraSession, exclusive: Bool) { + init(session: CameraSession, exclusive: Bool, additional: Bool) { self.session = session self.exclusive = exclusive + self.additional = additional self.output = CameraOutput(exclusive: exclusive) } @@ -145,13 +147,13 @@ private final class CameraContext { var ciImage = CIImage(cvImageBuffer: pixelBuffer) let size = ciImage.extent.size if mirror { - var transform = CGAffineTransformMakeScale(-1.0, 1.0) - transform = CGAffineTransformTranslate(transform, size.width, 0.0) + var transform = CGAffineTransformMakeScale(1.0, -1.0) + transform = CGAffineTransformTranslate(transform, 0.0, -size.height) ciImage = ciImage.transformed(by: transform) } ciImage = ciImage.clampedToExtent().applyingGaussianBlur(sigma: 40.0).cropped(to: CGRect(origin: .zero, size: size)) if let cgImage = self.cameraImageContext.createCGImage(ciImage, from: ciImage.extent) { - let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: additional ? .up : .right) + let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right) if additional { CameraSimplePreviewView.saveAdditionalLastStateImage(uiImage) } else { @@ -169,7 +171,7 @@ private final class CameraContext { self.simplePreviewView = previewView self.secondaryPreviewView = secondaryPreviewView - self.mainDeviceContext = CameraDeviceContext(session: session, exclusive: true) + self.mainDeviceContext = CameraDeviceContext(session: session, exclusive: true, additional: false) self.configure { self.mainDeviceContext.configure(position: configuration.position, previewView: self.simplePreviewView, audio: configuration.audio, photo: configuration.photo, metadata: configuration.metadata) } @@ -313,10 +315,10 @@ private final class CameraContext { if enabled { self.configure { self.mainDeviceContext.invalidate() - self.mainDeviceContext = CameraDeviceContext(session: self.session, exclusive: false) + self.mainDeviceContext = CameraDeviceContext(session: self.session, exclusive: false, additional: false) self.mainDeviceContext.configure(position: .back, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata) - self.additionalDeviceContext = CameraDeviceContext(session: self.session, exclusive: false) + self.additionalDeviceContext = CameraDeviceContext(session: self.session, exclusive: false, additional: true) self.additionalDeviceContext?.configure(position: .front, previewView: self.secondaryPreviewView, audio: false, photo: true, metadata: false) } self.mainDeviceContext.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in @@ -352,11 +354,11 @@ private final class CameraContext { } else { self.configure { self.mainDeviceContext.invalidate() - self.mainDeviceContext = CameraDeviceContext(session: self.session, exclusive: true) - self.mainDeviceContext.configure(position: .back, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata) - self.additionalDeviceContext?.invalidate() self.additionalDeviceContext = nil + + self.mainDeviceContext = CameraDeviceContext(session: self.session, exclusive: true, additional: false) + self.mainDeviceContext.configure(position: .back, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata) } self.mainDeviceContext.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in guard let self else { @@ -376,8 +378,30 @@ private final class CameraContext { } } - self.queue.after(0.5) { - self.modeChange = .none + if #available(iOS 13.0, *), let previewView = self.simplePreviewView { + if enabled, let secondaryPreviewView = self.secondaryPreviewView { + let _ = (combineLatest(previewView.isPreviewing, secondaryPreviewView.isPreviewing) + |> map { first, second in + return first && second + } + |> filter { $0 } + |> take(1) + |> delay(0.1, queue: self.queue) + |> deliverOn(self.queue)).start(next: { [weak self] _ in + self?.modeChange = .none + }) + } else { + let _ = (previewView.isPreviewing + |> filter { $0 } + |> take(1) + |> deliverOn(self.queue)).start(next: { [weak self] _ in + self?.modeChange = .none + }) + } + } else { + self.queue.after(0.4) { + self.modeChange = .none + } } } diff --git a/submodules/Camera/Sources/CameraPreviewView.swift b/submodules/Camera/Sources/CameraPreviewView.swift index c68f6d16a9..66ce27f886 100644 --- a/submodules/Camera/Sources/CameraPreviewView.swift +++ b/submodules/Camera/Sources/CameraPreviewView.swift @@ -42,9 +42,14 @@ public class CameraSimplePreviewView: UIView { } } + private let additional: Bool + private var previewingDisposable: Disposable? private let placeholderView = UIImageView() + public init(frame: CGRect, additional: Bool) { + self.additional = additional + super.init(frame: frame) self.videoPreviewLayer.videoGravity = .resizeAspectFill @@ -53,19 +58,17 @@ public class CameraSimplePreviewView: UIView { self.placeholderView.image = additional ? CameraSimplePreviewView.lastAdditionalStateImage() : CameraSimplePreviewView.lastStateImage() self.addSubview(self.placeholderView) - if #available(iOS 13.0, *) { - self.previewingDisposable = (self.isPreviewing - |> filter { $0 } - |> take(1) - |> deliverOnMainQueue).start(next: { [weak self] _ in - UIView.animate(withDuration: 0.3, delay: 0.15) { - self?.placeholderView.alpha = 0.0 - } - }) - } else { - Queue.mainQueue().after(0.5) { - UIView.animate(withDuration: 0.3) { - self.placeholderView.alpha = 0.0 + if !additional { + if #available(iOS 13.0, *) { + self.previewingDisposable = (self.isPreviewing + |> filter { $0 } + |> take(1) + |> deliverOnMainQueue).start(next: { [weak self] _ in + self?.removePlaceholder(delay: 0.15) + }) + } else { + Queue.mainQueue().after(0.35) { + self.removePlaceholder(delay: 0.15) } } } @@ -85,6 +88,20 @@ public class CameraSimplePreviewView: UIView { self.placeholderView.frame = self.bounds.insetBy(dx: -1.0, dy: -1.0) } + public func removePlaceholder(delay: Double = 0.0) { + UIView.animate(withDuration: 0.3, delay: delay) { + self.placeholderView.alpha = 0.0 + } + } + + public func resetPlaceholder() { + guard self.placeholderView.alpha == 0.0 else { + return + } + self.placeholderView.image = self.additional ? CameraSimplePreviewView.lastAdditionalStateImage() : CameraSimplePreviewView.lastStateImage() + self.placeholderView.alpha = 1.0 + } + private var _videoPreviewLayer: AVCaptureVideoPreviewLayer? var videoPreviewLayer: AVCaptureVideoPreviewLayer { if let layer = self._videoPreviewLayer { diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 6f581005a6..c968ce804e 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -158,7 +158,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController private var activeDownloadsDisposable: Disposable? private var clearUnseenDownloadsTimer: SwiftSignalKit.Timer? - private var isPremium: Bool = false + private(set) var isPremium: Bool = false private var didSetupTabs = false @@ -2341,6 +2341,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } fileprivate func openStoryCamera() { + guard self.isPremium else { + let context = self.context + var replaceImpl: ((ViewController) -> Void)? + let controller = context.sharedContext.makePremiumDemoController(context: self.context, subject: .stories, action: { + let controller = context.sharedContext.makePremiumIntroController(context: context, source: .stories) + replaceImpl?(controller) + }) + replaceImpl = { [weak controller] c in + controller?.replace(with: c) + } + self.push(controller) + return + } var cameraTransitionIn: StoryCameraTransitionIn? if let componentView = self.chatListHeaderView() { if let (transitionView, _) = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) { @@ -2506,8 +2519,8 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController }))) let isMuted = notificationSettings.storiesMuted == true - items.append(.action(ContextMenuActionItem(text: isMuted ? "Unmute" : "Mute", icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Muted": "Chat/Context Menu/Unmute"), color: theme.contextMenu.primaryColor) + items.append(.action(ContextMenuActionItem(text: isMuted ? "Notify" : "Not Notify", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, f in f(.default) diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index e958599146..6bfd3a3557 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1205,7 +1205,7 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range } - if case .compact = layout.metrics.widthClass { + if case .compact = layout.metrics.widthClass, self.controller?.isPremium == true { let cameraIsAlreadyOpened = self.controller?.hasStoryCameraTransition ?? false if selectedIndex <= 0 && translation.x > 0.0 { transitionFraction = 0.0 @@ -1218,6 +1218,11 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele transitionFraction = 0.0 return } + } else { + if selectedIndex <= 0 && translation.x > 0.0 { + let overscroll = translation.x + transitionFraction = rubberBandingOffset(offset: overscroll, bandingStart: 0.0) / layout.size.width + } } if selectedIndex >= maxFilterIndex && translation.x < 0.0 { diff --git a/submodules/ContactListUI/Sources/ContactsControllerNode.swift b/submodules/ContactListUI/Sources/ContactsControllerNode.swift index 5ca1d5f90a..37d384f473 100644 --- a/submodules/ContactListUI/Sources/ContactsControllerNode.swift +++ b/submodules/ContactListUI/Sources/ContactsControllerNode.swift @@ -68,6 +68,9 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { private var presentationDataDisposable: Disposable? private let stringsPromise = Promise() + private var isPremium = false + private var isPremiumDisposable: Disposable? + weak var controller: ContactsController? private var initialScrollingOffset: CGFloat? @@ -258,11 +261,22 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { self.storiesReady.set(.single(true)) }) + + self.isPremiumDisposable = (self.context.engine.data.subscribe(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId)) + |> map { + return $0?.isPremium ?? false + } + |> deliverOnMainQueue).start(next: { [weak self] isPremium in + if let self { + self.isPremium = isPremium + } + }) } deinit { self.presentationDataDisposable?.dispose() self.storySubscriptionsDisposable?.dispose() + self.isPremiumDisposable?.dispose() } override func didLoad() { @@ -293,6 +307,10 @@ final class ContactsControllerNode: ASDisplayNode, UIGestureRecognizerDelegate { return false } + override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { + return self.isPremium + } + private func updateThemeAndStrings() { self.backgroundColor = self.presentationData.theme.chatList.backgroundColor self.searchDisplayController?.updatePresentationData(self.presentationData) diff --git a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift index 2ef5236446..e153a645b6 100644 --- a/submodules/PremiumUI/Sources/PremiumDemoScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumDemoScreen.swift @@ -471,7 +471,7 @@ private final class DemoSheetContent: CombinedComponent { self.context = context self.subject = subject self.source = source - self.order = order ?? [.moreUpload, .fasterDownload, .voiceToText, .noAds, .uniqueReactions, .premiumStickers, .animatedEmoji, .advancedChatManagement, .profileBadge, .animatedUserpics, .appIcons, .translation] + self.order = order ?? [.moreUpload, .fasterDownload, .voiceToText, .noAds, .uniqueReactions, .premiumStickers, .animatedEmoji, .advancedChatManagement, .profileBadge, .animatedUserpics, .appIcons, .translation, .stories] self.action = action self.dismiss = dismiss } @@ -939,6 +939,25 @@ private final class DemoSheetContent: CombinedComponent { ) ) ) + //TODO:localize + availableItems[.stories] = DemoPagerComponent.Item( + AnyComponentWithIdentity( + id: PremiumDemoScreen.Subject.stories, + component: AnyComponent( + PageComponent( + content: AnyComponent(PhoneDemoComponent( + context: component.context, + position: .top, + videoFile: configuration.videos["voice_to_text"], + decoration: .badgeStars + )), + title: "Story Posting", + text: "Be one of the first to share your stories with your contacts or an unlimited audience.", + textColor: textColor + ) + ) + ) + ) var items: [DemoPagerComponent.Item] = component.order.compactMap { availableItems[$0] } let index: Int @@ -1029,6 +1048,10 @@ private final class DemoSheetContent: CombinedComponent { buttonAnimationName = "premium_unlock" case .translation: buttonText = strings.Premium_Translation_Proceed + case .stories: + //TODO:localize + buttonText = "Unlock Story Posting" + buttonAnimationName = "premium_unlock" default: buttonText = strings.Common_OK } @@ -1210,6 +1233,7 @@ public class PremiumDemoScreen: ViewControllerComponentContainer { case animatedEmoji case emojiStatus case translation + case stories } public enum Source: Equatable { diff --git a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift index 283828e28b..79c67b5d25 100644 --- a/submodules/PremiumUI/Sources/PremiumGiftScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumGiftScreen.swift @@ -397,6 +397,8 @@ private final class PremiumGiftScreenContentComponent: CombinedComponent { demoSubject = .emojiStatus case .translation: demoSubject = .translation + case .stories: + demoSubject = .stories } let buttonText: String diff --git a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift index 15bc133974..f18d56967b 100644 --- a/submodules/PremiumUI/Sources/PremiumIntroScreen.swift +++ b/submodules/PremiumUI/Sources/PremiumIntroScreen.swift @@ -184,6 +184,12 @@ public enum PremiumSource: Equatable { } else { return false } + case .stories: + if case .stories = rhs { + return true + } else { + return false + } } } @@ -213,63 +219,66 @@ public enum PremiumSource: Equatable { case translation case linksPerSharedFolder case membershipInSharedFolders + case stories var identifier: String? { switch self { - case .settings: - return "settings" - case .stickers: - return "premium_stickers" - case .reactions: - return "infinite_reactions" - case .ads: - return "no_ads" - case .upload: - return "more_upload" - case .appIcons: - return "app_icons" - case .groupsAndChannels: - return "double_limits__channels" - case .pinnedChats: - return "double_limits__dialog_pinned" - case .publicLinks: - return "double_limits__channels_public" - case .savedGifs: - return "double_limits__saved_gifs" - case .savedStickers: - return "double_limits__stickers_faved" - case .folders: - return "double_limits__dialog_filters" - case .chatsPerFolder: - return "double_limits__dialog_filters_chats" - case .accounts: - return "double_limits__accounts" - case .about: - return "double_limits__about" - case .animatedEmoji: - return "animated_emoji" - case let .profile(id): - return "profile__\(id.id._internalGetInt64Value())" - case .emojiStatus: - return "emoji_status" - case .voiceToText: - return "voice_to_text" - case .fasterDownload: - return "faster_download" - case .gift, .giftTerms: - return nil - case let .deeplink(reference): - if let reference = reference { - return "deeplink_\(reference)" - } else { - return "deeplink" - } - case .translation: - return "translations" - case .linksPerSharedFolder: - return "double_limits__community_invites" - case .membershipInSharedFolders: - return "double_limits__communities_joined" + case .settings: + return "settings" + case .stickers: + return "premium_stickers" + case .reactions: + return "infinite_reactions" + case .ads: + return "no_ads" + case .upload: + return "more_upload" + case .appIcons: + return "app_icons" + case .groupsAndChannels: + return "double_limits__channels" + case .pinnedChats: + return "double_limits__dialog_pinned" + case .publicLinks: + return "double_limits__channels_public" + case .savedGifs: + return "double_limits__saved_gifs" + case .savedStickers: + return "double_limits__stickers_faved" + case .folders: + return "double_limits__dialog_filters" + case .chatsPerFolder: + return "double_limits__dialog_filters_chats" + case .accounts: + return "double_limits__accounts" + case .about: + return "double_limits__about" + case .animatedEmoji: + return "animated_emoji" + case let .profile(id): + return "profile__\(id.id._internalGetInt64Value())" + case .emojiStatus: + return "emoji_status" + case .voiceToText: + return "voice_to_text" + case .fasterDownload: + return "faster_download" + case .gift, .giftTerms: + return nil + case let .deeplink(reference): + if let reference = reference { + return "deeplink_\(reference)" + } else { + return "deeplink" + } + case .translation: + return "translations" + case .linksPerSharedFolder: + return "double_limits__community_invites" + case .membershipInSharedFolders: + return "double_limits__communities_joined" + case .stories: + return "stories" } } } @@ -289,6 +298,7 @@ enum PremiumPerk: CaseIterable { case animatedEmoji case emojiStatus case translation + case stories static var allCases: [PremiumPerk] { return [ @@ -321,133 +331,142 @@ enum PremiumPerk: CaseIterable { var identifier: String { switch self { - case .doubleLimits: - return "double_limits" - case .moreUpload: - return "more_upload" - case .fasterDownload: - return "faster_download" - case .voiceToText: - return "voice_to_text" - case .noAds: - return "no_ads" - case .uniqueReactions: - return "infinite_reactions" - case .premiumStickers: - return "premium_stickers" - case .advancedChatManagement: - return "advanced_chat_management" - case .profileBadge: - return "profile_badge" - case .animatedUserpics: - return "animated_userpics" - case .appIcons: - return "app_icons" - case .animatedEmoji: - return "animated_emoji" - case .emojiStatus: - return "emoji_status" - case .translation: - return "translations" + case .doubleLimits: + return "double_limits" + case .moreUpload: + return "more_upload" + case .fasterDownload: + return "faster_download" + case .voiceToText: + return "voice_to_text" + case .noAds: + return "no_ads" + case .uniqueReactions: + return "infinite_reactions" + case .premiumStickers: + return "premium_stickers" + case .advancedChatManagement: + return "advanced_chat_management" + case .profileBadge: + return "profile_badge" + case .animatedUserpics: + return "animated_userpics" + case .appIcons: + return "app_icons" + case .animatedEmoji: + return "animated_emoji" + case .emojiStatus: + return "emoji_status" + case .translation: + return "translations" + case .stories: + return "stories" } } func title(strings: PresentationStrings) -> String { switch self { - case .doubleLimits: - return strings.Premium_DoubledLimits - case .moreUpload: - return strings.Premium_UploadSize - case .fasterDownload: - return strings.Premium_FasterSpeed - case .voiceToText: - return strings.Premium_VoiceToText - case .noAds: - return strings.Premium_NoAds - case .uniqueReactions: - return strings.Premium_InfiniteReactions - case .premiumStickers: - return strings.Premium_Stickers - case .advancedChatManagement: - return strings.Premium_ChatManagement - case .profileBadge: - return strings.Premium_Badge - case .animatedUserpics: - return strings.Premium_Avatar - case .appIcons: - return strings.Premium_AppIcon - case .animatedEmoji: - return strings.Premium_AnimatedEmoji - case .emojiStatus: - return strings.Premium_EmojiStatus - case .translation: - return strings.Premium_Translation + case .doubleLimits: + return strings.Premium_DoubledLimits + case .moreUpload: + return strings.Premium_UploadSize + case .fasterDownload: + return strings.Premium_FasterSpeed + case .voiceToText: + return strings.Premium_VoiceToText + case .noAds: + return strings.Premium_NoAds + case .uniqueReactions: + return strings.Premium_InfiniteReactions + case .premiumStickers: + return strings.Premium_Stickers + case .advancedChatManagement: + return strings.Premium_ChatManagement + case .profileBadge: + return strings.Premium_Badge + case .animatedUserpics: + return strings.Premium_Avatar + case .appIcons: + return strings.Premium_AppIcon + case .animatedEmoji: + return strings.Premium_AnimatedEmoji + case .emojiStatus: + return strings.Premium_EmojiStatus + case .translation: + return strings.Premium_Translation + case .stories: + //TODO:localize + return "Story Posting" } } func subtitle(strings: PresentationStrings) -> String { switch self { - case .doubleLimits: - return strings.Premium_DoubledLimitsInfo - case .moreUpload: - return strings.Premium_UploadSizeInfo - case .fasterDownload: - return strings.Premium_FasterSpeedInfo - case .voiceToText: - return strings.Premium_VoiceToTextInfo - case .noAds: - return strings.Premium_NoAdsInfo - case .uniqueReactions: - return strings.Premium_InfiniteReactionsInfo - case .premiumStickers: - return strings.Premium_StickersInfo - case .advancedChatManagement: - return strings.Premium_ChatManagementInfo - case .profileBadge: - return strings.Premium_BadgeInfo - case .animatedUserpics: - return strings.Premium_AvatarInfo - case .appIcons: - return strings.Premium_AppIconInfo - case .animatedEmoji: - return strings.Premium_AnimatedEmojiInfo - case .emojiStatus: - return strings.Premium_EmojiStatusInfo - case .translation: - return strings.Premium_TranslationInfo + case .doubleLimits: + return strings.Premium_DoubledLimitsInfo + case .moreUpload: + return strings.Premium_UploadSizeInfo + case .fasterDownload: + return strings.Premium_FasterSpeedInfo + case .voiceToText: + return strings.Premium_VoiceToTextInfo + case .noAds: + return strings.Premium_NoAdsInfo + case .uniqueReactions: + return strings.Premium_InfiniteReactionsInfo + case .premiumStickers: + return strings.Premium_StickersInfo + case .advancedChatManagement: + return strings.Premium_ChatManagementInfo + case .profileBadge: + return strings.Premium_BadgeInfo + case .animatedUserpics: + return strings.Premium_AvatarInfo + case .appIcons: + return strings.Premium_AppIconInfo + case .animatedEmoji: + return strings.Premium_AnimatedEmojiInfo + case .emojiStatus: + return strings.Premium_EmojiStatusInfo + case .translation: + return strings.Premium_TranslationInfo + case .stories: + return "Be one of the first to share your stories with your contacts or an unlimited audience." } } var iconName: String { switch self { - case .doubleLimits: - return "Premium/Perk/Limits" - case .moreUpload: - return "Premium/Perk/Upload" - case .fasterDownload: - return "Premium/Perk/Speed" - case .voiceToText: - return "Premium/Perk/Voice" - case .noAds: - return "Premium/Perk/NoAds" - case .uniqueReactions: - return "Premium/Perk/Reactions" - case .premiumStickers: - return "Premium/Perk/Stickers" - case .advancedChatManagement: - return "Premium/Perk/Chat" - case .profileBadge: - return "Premium/Perk/Badge" - case .animatedUserpics: - return "Premium/Perk/Avatar" - case .appIcons: - return "Premium/Perk/AppIcon" - case .animatedEmoji: - return "Premium/Perk/Emoji" - case .emojiStatus: - return "Premium/Perk/Status" - case .translation: - return "Premium/Perk/Translation" + case .doubleLimits: + return "Premium/Perk/Limits" + case .moreUpload: + return "Premium/Perk/Upload" + case .fasterDownload: + return "Premium/Perk/Speed" + case .voiceToText: + return "Premium/Perk/Voice" + case .noAds: + return "Premium/Perk/NoAds" + case .uniqueReactions: + return "Premium/Perk/Reactions" + case .premiumStickers: + return "Premium/Perk/Stickers" + case .advancedChatManagement: + return "Premium/Perk/Chat" + case .profileBadge: + return "Premium/Perk/Badge" + case .animatedUserpics: + return "Premium/Perk/Avatar" + case .appIcons: + return "Premium/Perk/AppIcon" + case .animatedEmoji: + return "Premium/Perk/Emoji" + case .emojiStatus: + return "Premium/Perk/Status" + case .translation: + return "Premium/Perk/Translation" + case .stories: + return "Premium/Perk/Translation" } } } @@ -1714,6 +1733,8 @@ private final class PremiumIntroScreenContentComponent: CombinedComponent { demoSubject = .emojiStatus case .translation: demoSubject = .translation + case .stories: + demoSubject = .stories } let isPremium = state?.isPremium == true diff --git a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift index 8137a77fce..a4fd22e857 100644 --- a/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift +++ b/submodules/ReactionSelectionNode/Sources/ReactionContextNode.swift @@ -217,7 +217,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { private var highlightedByHover = false private var didTriggerExpandedReaction: Bool = false private var continuousHaptic: Any? - private var validLayout: (CGSize, UIEdgeInsets, CGRect, Bool)? + private var validLayout: (CGSize, UIEdgeInsets, CGRect, Bool, Bool)? private var isLeftAligned: Bool = true private var itemLayout: ItemLayout? @@ -603,8 +603,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } - public func updateLayout(size: CGSize, insets: UIEdgeInsets, anchorRect: CGRect, isCoveredByInput: Bool, isAnimatingOut: Bool, transition: ContainedViewLayoutTransition) { - self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, isCoveredByInput: isCoveredByInput, isAnimatingOut: isAnimatingOut, transition: transition, animateInFromAnchorRect: nil, animateOutToAnchorRect: nil) + public func updateLayout(size: CGSize, insets: UIEdgeInsets, anchorRect: CGRect, centerAligned: Bool = false, isCoveredByInput: Bool, isAnimatingOut: Bool, transition: ContainedViewLayoutTransition) { + self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, centerAligned: centerAligned, isCoveredByInput: isCoveredByInput, isAnimatingOut: isAnimatingOut, transition: transition, animateInFromAnchorRect: nil, animateOutToAnchorRect: nil) } public func updateIsIntersectingContent(isIntersectingContent: Bool, transition: ContainedViewLayoutTransition) { @@ -615,8 +615,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if self.extensionDistance != distance { self.extensionDistance = distance - if let (size, insets, anchorRect, isCoveredByInput) = self.validLayout { - self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, isCoveredByInput: isCoveredByInput, isAnimatingOut: false, transition: .immediate, animateInFromAnchorRect: nil, animateOutToAnchorRect: nil) + if let (size, insets, anchorRect, isCoveredByInput, centerAligned) = self.validLayout { + self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, centerAligned: centerAligned, isCoveredByInput: isCoveredByInput, isAnimatingOut: false, transition: .immediate, animateInFromAnchorRect: nil, animateOutToAnchorRect: nil) } } } @@ -629,7 +629,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } - private func calculateBackgroundFrame(containerSize: CGSize, insets: UIEdgeInsets, anchorRect: CGRect, contentSize: CGSize) -> (backgroundFrame: CGRect, visualBackgroundFrame: CGRect, isLeftAligned: Bool, cloudSourcePoint: CGFloat) { + private func calculateBackgroundFrame(containerSize: CGSize, insets: UIEdgeInsets, anchorRect: CGRect, contentSize: CGSize, centerAligned: Bool) -> (backgroundFrame: CGRect, visualBackgroundFrame: CGRect, isLeftAligned: Bool, cloudSourcePoint: CGFloat) { var contentSize = contentSize contentSize.width = max(46.0, contentSize.width) contentSize.height = self.currentContentHeight @@ -658,6 +658,10 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { rect.origin.x = sideInset } + if centerAligned { + rect.origin.x = floor((containerSize.width - rect.width) / 2.0) + } + let cloudSourcePoint: CGFloat if isLeftAligned { cloudSourcePoint = min(rect.maxX - 46.0 / 2.0, anchorRect.maxX - 4.0) @@ -972,12 +976,12 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { } } - private func updateLayout(size: CGSize, insets: UIEdgeInsets, anchorRect: CGRect, isCoveredByInput: Bool, isAnimatingOut: Bool, transition: ContainedViewLayoutTransition, animateInFromAnchorRect: CGRect?, animateOutToAnchorRect: CGRect?, animateReactionHighlight: Bool = false) { + private func updateLayout(size: CGSize, insets: UIEdgeInsets, anchorRect: CGRect, centerAligned: Bool, isCoveredByInput: Bool, isAnimatingOut: Bool, transition: ContainedViewLayoutTransition, animateInFromAnchorRect: CGRect?, animateOutToAnchorRect: CGRect?, animateReactionHighlight: Bool = false) { if let expandItemView = self.expandItemView { expandItemView.updateTheme(theme: self.presentationData.theme) } - self.validLayout = (size, insets, anchorRect, isCoveredByInput) + self.validLayout = (size, insets, anchorRect, isCoveredByInput, centerAligned) let externalSideInset: CGFloat = 4.0 let sideInset: CGFloat = 6.0 @@ -1030,7 +1034,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { backgroundInsets.left += sideInset backgroundInsets.right += sideInset - let (actualBackgroundFrame, visualBackgroundFrame, isLeftAligned, cloudSourcePoint) = self.calculateBackgroundFrame(containerSize: CGSize(width: size.width, height: size.height), insets: backgroundInsets, anchorRect: anchorRect, contentSize: CGSize(width: visibleContentWidth, height: contentHeight)) + let (actualBackgroundFrame, visualBackgroundFrame, isLeftAligned, cloudSourcePoint) = self.calculateBackgroundFrame(containerSize: CGSize(width: size.width, height: size.height), insets: backgroundInsets, anchorRect: anchorRect, contentSize: CGSize(width: visibleContentWidth, height: contentHeight), centerAligned: centerAligned) self.isLeftAligned = isLeftAligned self.itemLayout = ItemLayout( @@ -1199,7 +1203,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { let springScaleDelay: Double = 0.1 let springDelay: Double = springScaleDelay + 0.01 - let sourceBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, insets: backgroundInsets, anchorRect: animateInFromAnchorRect, contentSize: CGSize(width: visualBackgroundFrame.height, height: contentHeight)).0 + let sourceBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, insets: backgroundInsets, anchorRect: animateInFromAnchorRect, contentSize: CGSize(width: visualBackgroundFrame.height, height: contentHeight), centerAligned: false).0 self.backgroundNode.animateInFromAnchorRect(size: visualBackgroundFrame.size, sourceBackgroundFrame: sourceBackgroundFrame.offsetBy(dx: -visualBackgroundFrame.minX, dy: -visualBackgroundFrame.minY)) @@ -1210,7 +1214,7 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.contentTintContainer.layer.animateSpring(from: NSValue(cgPoint: CGPoint(x: sourceBackgroundFrame.midX - visualBackgroundFrame.midX, y: 0.0)), to: NSValue(cgPoint: CGPoint()), keyPath: "position", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping, additive: true) self.contentTintContainer.layer.animateSpring(from: NSValue(cgRect: CGRect(origin: CGPoint(x: (sourceBackgroundFrame.minX - visualBackgroundFrame.minX), y: 0.0), size: sourceBackgroundFrame.size)), to: NSValue(cgRect: CGRect(origin: CGPoint(), size: visualBackgroundFrame.size)), keyPath: "bounds", duration: springDuration, delay: springDelay, initialVelocity: 0.0, damping: springDamping) } else if let animateOutToAnchorRect = animateOutToAnchorRect { - let targetBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, insets: backgroundInsets, anchorRect: animateOutToAnchorRect, contentSize: CGSize(width: visibleContentWidth, height: contentHeight)).0 + let targetBackgroundFrame = self.calculateBackgroundFrame(containerSize: size, insets: backgroundInsets, anchorRect: animateOutToAnchorRect, contentSize: CGSize(width: visibleContentWidth, height: contentHeight), centerAligned: false).0 let offset = CGPoint(x: -(targetBackgroundFrame.minX - visualBackgroundFrame.minX), y: -(targetBackgroundFrame.minY - visualBackgroundFrame.minY)) self.position = CGPoint(x: self.position.x - offset.x, y: self.position.y - offset.y) @@ -1596,8 +1600,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { public func animateIn(from sourceAnchorRect: CGRect) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.15) - if let (size, insets, anchorRect, isCoveredByInput) = self.validLayout { - self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, isCoveredByInput: isCoveredByInput, isAnimatingOut: false, transition: .immediate, animateInFromAnchorRect: sourceAnchorRect, animateOutToAnchorRect: nil) + if let (size, insets, anchorRect, isCoveredByInput, centerAligned) = self.validLayout { + self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, centerAligned: centerAligned, isCoveredByInput: isCoveredByInput, isAnimatingOut: false, transition: .immediate, animateInFromAnchorRect: sourceAnchorRect, animateOutToAnchorRect: nil) } let mainCircleDelay: Double = 0.01 @@ -1681,8 +1685,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { expandItemView.tintView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2) } - if let targetAnchorRect = targetAnchorRect, let (size, insets, anchorRect, isCoveredByInput) = self.validLayout { - self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, isCoveredByInput: isCoveredByInput, isAnimatingOut: false, transition: .immediate, animateInFromAnchorRect: nil, animateOutToAnchorRect: targetAnchorRect) + if let targetAnchorRect = targetAnchorRect, let (size, insets, anchorRect, isCoveredByInput, centerAligned) = self.validLayout { + self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, centerAligned: centerAligned, isCoveredByInput: isCoveredByInput, isAnimatingOut: false, transition: .immediate, animateInFromAnchorRect: nil, animateOutToAnchorRect: targetAnchorRect) } } @@ -2134,8 +2138,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.hapticFeedback = HapticFeedback() } - if let (size, insets, anchorRect, isCoveredByInput) = self.validLayout { - self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, isCoveredByInput: isCoveredByInput, isAnimatingOut: false, transition: .animated(duration: longPressDuration, curve: .linear), animateInFromAnchorRect: nil, animateOutToAnchorRect: nil, animateReactionHighlight: true) + if let (size, insets, anchorRect, isCoveredByInput, centerAligned) = self.validLayout { + self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, centerAligned: centerAligned, isCoveredByInput: isCoveredByInput, isAnimatingOut: false, transition: .animated(duration: longPressDuration, curve: .linear), animateInFromAnchorRect: nil, animateOutToAnchorRect: nil, animateReactionHighlight: true) } self.longPressTimer?.invalidate() @@ -2165,8 +2169,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.continuousHaptic = nil self.highlightedReaction = nil - if let (size, insets, anchorRect, isCoveredByInput) = self.validLayout { - self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, isCoveredByInput: isCoveredByInput, isAnimatingOut: false, transition: .animated(duration: 0.3, curve: .spring), animateInFromAnchorRect: nil, animateOutToAnchorRect: nil, animateReactionHighlight: true) + if let (size, insets, anchorRect, isCoveredByInput, centerAligned) = self.validLayout { + self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, centerAligned: centerAligned, isCoveredByInput: isCoveredByInput, isAnimatingOut: false, transition: .animated(duration: 0.3, curve: .spring), animateInFromAnchorRect: nil, animateOutToAnchorRect: nil, animateReactionHighlight: true) } case .ended: self.longPressTimer?.invalidate() @@ -2242,8 +2246,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { self.hapticFeedback?.tap() } - if let (size, insets, anchorRect, isCoveredByInput) = self.validLayout { - self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, isCoveredByInput: isCoveredByInput, isAnimatingOut: false, transition: .animated(duration: 0.18, curve: .easeInOut), animateInFromAnchorRect: nil, animateOutToAnchorRect: nil, animateReactionHighlight: true) + if let (size, insets, anchorRect, isCoveredByInput, centerAligned) = self.validLayout { + self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, centerAligned: centerAligned, isCoveredByInput: isCoveredByInput, isAnimatingOut: false, transition: .animated(duration: 0.18, curve: .easeInOut), animateInFromAnchorRect: nil, animateOutToAnchorRect: nil, animateReactionHighlight: true) } } } @@ -2258,8 +2262,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { if performAction { self.performReactionSelection(reaction: highlightedReaction, isLarge: isLarge) } else { - if let (size, insets, anchorRect, isCoveredByInput) = self.validLayout { - self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, isCoveredByInput: isCoveredByInput, isAnimatingOut: false, transition: .animated(duration: 0.18, curve: .easeInOut), animateInFromAnchorRect: nil, animateOutToAnchorRect: nil, animateReactionHighlight: true) + if let (size, insets, anchorRect, isCoveredByInput, centerAligned) = self.validLayout { + self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, centerAligned: centerAligned, isCoveredByInput: isCoveredByInput, isAnimatingOut: false, transition: .animated(duration: 0.18, curve: .easeInOut), animateInFromAnchorRect: nil, animateOutToAnchorRect: nil, animateReactionHighlight: true) } } } @@ -2356,8 +2360,8 @@ public final class ReactionContextNode: ASDisplayNode, UIScrollViewDelegate { public func setHighlightedReaction(_ value: ReactionItem.Reaction?) { self.highlightedReaction = value - if let (size, insets, anchorRect, isCoveredByInput) = self.validLayout { - self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, isCoveredByInput: isCoveredByInput, isAnimatingOut: false, transition: .animated(duration: 0.18, curve: .easeInOut), animateInFromAnchorRect: nil, animateOutToAnchorRect: nil, animateReactionHighlight: true) + if let (size, insets, anchorRect, isCoveredByInput, centerAligned) = self.validLayout { + self.updateLayout(size: size, insets: insets, anchorRect: anchorRect, centerAligned: centerAligned, isCoveredByInput: isCoveredByInput, isAnimatingOut: false, transition: .animated(duration: 0.18, curve: .easeInOut), animateInFromAnchorRect: nil, animateOutToAnchorRect: nil, animateReactionHighlight: true) } } } diff --git a/submodules/TelegramApi/Sources/Api31.swift b/submodules/TelegramApi/Sources/Api31.swift index 7f56233329..705c981590 100644 --- a/submodules/TelegramApi/Sources/Api31.swift +++ b/submodules/TelegramApi/Sources/Api31.swift @@ -8667,6 +8667,28 @@ public extension Api.functions.stories { }) } } +public extension Api.functions.stories { + static func report(userId: Api.InputUser, id: [Int32], reason: Api.ReportReason, message: String) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-916725654) + userId.serialize(buffer, true) + buffer.appendInt32(481674261) + buffer.appendInt32(Int32(id.count)) + for item in id { + serializeInt32(item, buffer: buffer, boxed: false) + } + reason.serialize(buffer, true) + serializeString(message, buffer: buffer, boxed: false) + return (FunctionDescription(name: "stories.report", parameters: [("userId", String(describing: userId)), ("id", String(describing: id)), ("reason", String(describing: reason)), ("message", String(describing: message))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Bool? in + let reader = BufferReader(buffer) + var result: Api.Bool? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Bool + } + return result + }) + } +} public extension Api.functions.stories { static func sendStory(flags: Int32, media: Api.InputMedia, caption: String?, entities: [Api.MessageEntity]?, privacyRules: [Api.InputPrivacyRule], randomId: Int64, period: Int32?) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift index aa96854d91..faae1436b3 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/ReportPeer.swift @@ -170,6 +170,22 @@ func _internal_reportPeerMessages(account: Account, messageIds: [MessageId], rea } |> switchToLatest } +func _internal_reportPeerStory(account: Account, peerId: PeerId, storyId: Int32, reason: ReportReason, message: String) -> Signal { + return account.postbox.transaction { transaction -> Signal in + if let peer = transaction.getPeer(peerId), let inputUser = apiInputUser(peer) { + return account.network.request(Api.functions.stories.report(userId: inputUser, id: [storyId], reason: reason.apiReason, message: message)) + |> `catch` { _ -> Signal in + return .single(.boolFalse) + } + |> mapToSignal { _ -> Signal in + return .complete() + } + } else { + return .complete() + } + } |> switchToLatest +} + func _internal_reportPeerReaction(account: Account, authorId: PeerId, messageId: MessageId) -> Signal { return account.postbox.transaction { transaction -> (Api.InputPeer, Api.InputPeer)? in guard let peer = transaction.getPeer(messageId.peerId).flatMap(apiInputPeer) else { diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift index 14a1a6a2df..109c5caba8 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Peers/TelegramEnginePeers.swift @@ -231,6 +231,10 @@ public extension TelegramEngine { return _internal_reportPeerMessages(account: self.account, messageIds: messageIds, reason: reason, message: message) } + public func reportPeerStory(peerId: PeerId, storyId: Int32, reason: ReportReason, message: String) -> Signal { + return _internal_reportPeerStory(account: self.account, peerId: peerId, storyId: storyId, reason: reason, message: message) + } + public func reportPeerReaction(authorId: PeerId, messageId: MessageId) -> Signal { return _internal_reportPeerReaction(account: self.account, authorId: authorId, messageId: messageId) } diff --git a/submodules/TelegramUI/BUILD b/submodules/TelegramUI/BUILD index e3fc89a6c6..a2e02b02c6 100644 --- a/submodules/TelegramUI/BUILD +++ b/submodules/TelegramUI/BUILD @@ -377,6 +377,7 @@ swift_library( "//submodules/TelegramUI/Components/Stories/StorySetIndicatorComponent", "//submodules/TelegramUI/Components/Chat/ChatMessageForwardInfoNode", "//submodules/TelegramUI/Components/Chat/ChatAvatarNavigationNode", + "//submodules/TelegramUI/Components/PeerReportScreen", "//submodules/Utils/VolumeButtons", "//submodules/ChatContextQuery", ] + select({ diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 6836895cfd..ba56526762 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -877,6 +877,7 @@ public class CameraScreen: ViewController { private var changingPositionDisposable: Disposable? private var isDualCamEnabled = false + private var appliedDualCam = false private var cameraPosition: Camera.Position = .back private let completion = ActionSlot>() @@ -1069,6 +1070,10 @@ public class CameraScreen: ViewController { previewSnapshotView.removeFromSuperview() }) } + + if self.isDualCamEnabled { + self.additionalPreviewView?.removePlaceholder() + } } } }) @@ -1115,8 +1120,10 @@ public class CameraScreen: ViewController { if let self { let previousPosition = self.cameraPosition self.cameraPosition = state.position - self.isDualCamEnabled = state.isDualCamEnabled + let dualCamWasEnabled = self.isDualCamEnabled + self.isDualCamEnabled = state.isDualCamEnabled + if self.isDualCamEnabled && previousPosition != state.position, let additionalPreviewView = self.additionalPreviewView { if state.position == .front { additionalPreviewView.superview?.sendSubviewToBack(additionalPreviewView) @@ -1128,6 +1135,9 @@ public class CameraScreen: ViewController { self.requestUpdateLayout(hasAppeared: false, transition: .immediate) CATransaction.commit() } else { + if !dualCamWasEnabled && self.isDualCamEnabled { + + } self.requestUpdateLayout(hasAppeared: false, transition: .spring(duration: 0.4)) } } @@ -1599,8 +1609,10 @@ public class CameraScreen: ViewController { transition.setFrame(view: self.currentPreviewView, frame: CGRect(origin: .zero, size: previewFrame.size)) transition.setFrame(view: self.previewBlurView, frame: CGRect(origin: .zero, size: previewFrame.size)) - - if let additionalPreviewView = self.currentAdditionalPreviewView { + if let additionalPreviewView = self.currentAdditionalPreviewView as? CameraSimplePreviewView { + let dualCamUpdated = self.appliedDualCam != self.isDualCamEnabled + self.appliedDualCam = self.isDualCamEnabled + additionalPreviewView.layer.cornerRadius = 80.0 var origin: CGPoint @@ -1637,6 +1649,12 @@ public class CameraScreen: ViewController { transition.setScale(view: additionalPreviewView, scale: self.isDualCamEnabled ? 1.0 : 0.1) transition.setAlpha(view: additionalPreviewView, alpha: self.isDualCamEnabled ? 1.0 : 0.0) + + if dualCamUpdated && !self.isDualCamEnabled { + Queue.mainQueue().after(0.5) { + additionalPreviewView.resetPlaceholder() + } + } } self.previewFrameLeftDimView.isHidden = !isTablet diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift index 7ac2d4f84c..9aaa6a1334 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift @@ -4,6 +4,14 @@ import Display import AccountContext import TelegramCore +private func entitiesPath() -> String { + return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/mediaEntities" +} + +private func fullEntityMediaPath(_ path: String) -> String { + return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0] + "/mediaEntities/" + path +} + public final class DrawingStickerEntity: DrawingEntity, Codable { public enum Content: Equatable { case file(TelegramMediaFile) @@ -36,9 +44,9 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { private enum CodingKeys: String, CodingKey { case uuid case file - case image + case imagePath case videoPath - case videoImage + case videoImagePath case referenceDrawingSize case position case scale @@ -101,11 +109,11 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { self.uuid = try container.decode(UUID.self, forKey: .uuid) if let file = try container.decodeIfPresent(TelegramMediaFile.self, forKey: .file) { self.content = .file(file) - } else if let imageData = try container.decodeIfPresent(Data.self, forKey: .image), let image = UIImage(data: imageData) { + } else if let imagePath = try container.decodeIfPresent(String.self, forKey: .imagePath), let image = UIImage(contentsOfFile: fullEntityMediaPath(imagePath)) { self.content = .image(image) } else if let videoPath = try container.decodeIfPresent(String.self, forKey: .videoPath) { var imageValue: UIImage? - if let imageData = try container.decodeIfPresent(Data.self, forKey: .image), let image = UIImage(data: imageData) { + if let imagePath = try container.decodeIfPresent(String.self, forKey: .videoImagePath), let image = UIImage(contentsOfFile: fullEntityMediaPath(imagePath)) { imageValue = image } self.content = .video(videoPath, imageValue) @@ -126,10 +134,22 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { case let .file(file): try container.encode(file, forKey: .file) case let .image(image): - try container.encodeIfPresent(image.pngData(), forKey: .image) + let imagePath = "\(self.uuid).png" + let fullImagePath = fullEntityMediaPath(imagePath) + if let imageData = image.pngData() { + try? FileManager.default.createDirectory(atPath: entitiesPath(), withIntermediateDirectories: true) + try? imageData.write(to: URL(fileURLWithPath: fullImagePath)) + try container.encodeIfPresent(imagePath, forKey: .imagePath) + } case let .video(path, image): try container.encode(path, forKey: .videoPath) - try container.encodeIfPresent(image?.jpegData(compressionQuality: 0.87), forKey: .videoImage) + let imagePath = "\(self.uuid).jpg" + let fullImagePath = fullEntityMediaPath(imagePath) + if let imageData = image?.jpegData(compressionQuality: 0.87) { + try? FileManager.default.createDirectory(atPath: entitiesPath(), withIntermediateDirectories: true) + try? imageData.write(to: URL(fileURLWithPath: fullImagePath)) + try container.encodeIfPresent(imagePath, forKey: .videoImagePath) + } } try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) try container.encode(self.position, forKey: .position) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 963c6cf81a..6ee138514a 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -888,7 +888,7 @@ final class MediaEditorScreenComponent: Component { timeoutSelected = false var inputPanelAvailableWidth = previewSize.width - var inputPanelAvailableHeight = 115.0 + var inputPanelAvailableHeight = 103.0 if case .regular = environment.metrics.widthClass { if (self.inputPanelExternalState.isEditing || self.inputPanelExternalState.hasText) { inputPanelAvailableWidth += 200.0 @@ -968,6 +968,7 @@ final class MediaEditorScreenComponent: Component { }) }, forwardAction: nil, + moreAction: nil, presentVoiceMessagesUnavailableTooltip: nil, audioRecorder: nil, videoRecordingStatus: nil, @@ -1547,6 +1548,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate private var isDismissing = false private var dismissOffset: CGFloat = 0.0 private var isDismissed = false + private var isDismissBySwipeSuppressed = false private var presentationData: PresentationData private var validLayout: ContainerViewLayout? @@ -1955,6 +1957,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if abs(translation.y) > 10.0 && !self.isEnhancing && hasSwipeToDismiss { if !self.isDismissing { self.isDismissing = true + self.isDismissBySwipeSuppressed = controller.isEligibleForDraft() controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut)) } } else if abs(translation.x) > 10.0 && !self.isDismissing { @@ -1965,6 +1968,12 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate if self.isDismissing { self.dismissOffset = translation.y controller.requestLayout(transition: .immediate) + + if abs(self.dismissOffset) > 20.0, controller.isEligibleForDraft() { + gestureRecognizer.isEnabled = false + gestureRecognizer.isEnabled = true + controller.maybePresentDiscardAlert() + } } else if self.isEnhancing { if let mediaEditor = self.mediaEditor { let value = mediaEditor.getToolValue(.enhance) as? Float ?? 0.0 @@ -1977,7 +1986,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } case .ended, .cancelled: if self.isDismissing { - if abs(translation.y) > self.view.frame.height * 0.33 || abs(velocity.y) > 1000.0 { + if abs(translation.y) > self.view.frame.height * 0.33 || abs(velocity.y) > 1000.0, !controller.isEligibleForDraft() { controller.requestDismiss(saveDraft: false, animated: true) } else { self.dismissOffset = 0.0 @@ -2532,7 +2541,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate isInteractingWithEntities: self.isInteractingWithEntities, isSavingAvailable: controller.isSavingAvailable, hasAppeared: self.hasAppeared, - isDismissing: self.isDismissing, + isDismissing: self.isDismissing && !self.isDismissBySwipeSuppressed, bottomSafeInset: layout.intrinsicInsets.bottom, mediaEditor: self.mediaEditor, privacy: controller.state.privacy, @@ -2697,7 +2706,7 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate } transition.setFrame(view: self.backgroundDimView, frame: CGRect(origin: .zero, size: layout.size)) - transition.setAlpha(view: self.backgroundDimView, alpha: self.isDismissing ? 0.0 : 1.0) + transition.setAlpha(view: self.backgroundDimView, alpha: self.isDismissing && !self.isDismissBySwipeSuppressed ? 0.0 : 1.0) var bottomInputOffset: CGFloat = 0.0 if inputHeight > 0.0 { @@ -3042,19 +3051,27 @@ public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate self.present(controller, in: .current) } - func maybePresentDiscardAlert() { + func isEligibleForDraft() -> Bool { guard let mediaEditor = self.node.mediaEditor else { - return + return false } let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) } let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView) mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities) - self.hapticFeedback.impact(.light) if let subject = self.node.subject, case .asset = subject, self.node.mediaEditor?.values.hasChanges == false { + return false + } + return true + } + + func maybePresentDiscardAlert() { + self.hapticFeedback.impact(.light) + if !self.isEligibleForDraft() { self.requestDismiss(saveDraft: false, animated: true) return } + let title: String let save: String if case .draft = self.node.subject { diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift index 54d8fb7528..f7f36d7ebd 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/StoryPreviewComponent.swift @@ -264,7 +264,8 @@ final class StoryPreviewComponent: Component { attachmentAction: { }, inputModeAction: nil, timeoutAction: nil, - forwardAction: nil, + forwardAction: {}, + moreAction: { _, _ in }, presentVoiceMessagesUnavailableTooltip: nil, audioRecorder: nil, videoRecordingStatus: nil, diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD b/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD index aec71a56c8..f48ad6e74c 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/BUILD @@ -27,6 +27,7 @@ swift_library( "//submodules/ChatContextQuery", "//submodules/TextFormat", "//submodules/TelegramUI/Components/Stories/PeerListItemComponent", + "//submodules/TelegramUI/Components/MoreHeaderButton", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift index 0c7df6c5db..c02624081e 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputActionButtonComponent.swift @@ -7,6 +7,7 @@ import ChatTextInputMediaRecordingButton import AccountContext import TelegramPresentationData import ChatPresentationInterfaceState +import MoreHeaderButton private extension MessageInputActionButtonComponent.Mode { var iconName: String? { @@ -34,6 +35,7 @@ public final class MessageInputActionButtonComponent: Component { case delete case attach case forward + case more } public enum Action { @@ -47,6 +49,7 @@ public final class MessageInputActionButtonComponent: Component { public let updateMediaCancelFraction: (CGFloat) -> Void public let lockMediaRecording: () -> Void public let stopAndPreviewMediaRecording: () -> Void + public let moreAction: (UIView, ContextGesture?) -> Void public let context: AccountContext public let theme: PresentationTheme public let strings: PresentationStrings @@ -61,6 +64,7 @@ public final class MessageInputActionButtonComponent: Component { updateMediaCancelFraction: @escaping (CGFloat) -> Void, lockMediaRecording: @escaping () -> Void, stopAndPreviewMediaRecording: @escaping () -> Void, + moreAction: @escaping (UIView, ContextGesture?) -> Void, context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, @@ -74,6 +78,7 @@ public final class MessageInputActionButtonComponent: Component { self.updateMediaCancelFraction = updateMediaCancelFraction self.lockMediaRecording = lockMediaRecording self.stopAndPreviewMediaRecording = stopAndPreviewMediaRecording + self.moreAction = moreAction self.context = context self.theme = theme self.strings = strings @@ -107,6 +112,7 @@ public final class MessageInputActionButtonComponent: Component { public final class View: HighlightTrackingButton { private var micButton: ChatTextInputMediaRecordingButton? private let sendIconView: UIImageView + private var moreButton: MoreHeaderButton? private var component: MessageInputActionButtonComponent? private weak var componentState: EmptyComponentState? @@ -228,20 +234,47 @@ public final class MessageInputActionButtonComponent: Component { } } + if self.moreButton == nil { + let moreButton = MoreHeaderButton(color: .white) + self.moreButton = moreButton + self.addSubnode(moreButton) + + moreButton.isUserInteractionEnabled = true + moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: .white))) + moreButton.onPressed = { [weak self] in + guard let self, let component = self.component, let moreButton = self.moreButton else { + return + } + moreButton.play() + component.moreAction(moreButton.view, nil) + } + moreButton.contextAction = { [weak self] sourceNode, gesture in + guard let self, let component = self.component, let moreButton = self.moreButton else { + return + } + moreButton.play() + component.moreAction(moreButton.view, gesture) + } + self.moreButton = moreButton + self.addSubnode(moreButton) + } + var sendAlpha: CGFloat = 0.0 var microphoneAlpha: CGFloat = 0.0 + var moreAlpha: CGFloat = 0.0 switch component.mode { case .none: break case .send, .apply, .attach, .delete, .forward: sendAlpha = 1.0 + case .more: + moreAlpha = 1.0 case .videoInput, .voiceInput: microphoneAlpha = 1.0 case .unavailableVoiceInput: microphoneAlpha = 0.4 } - if self.sendIconView.image == nil || previousComponent?.mode.iconName != component.mode.iconName { if let iconName = component.mode.iconName { self.sendIconView.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: .white) @@ -310,6 +343,17 @@ public final class MessageInputActionButtonComponent: Component { transition.setBounds(view: self.sendIconView, bounds: CGRect(origin: CGPoint(), size: iconFrame.size)) } + if let moreButton = self.moreButton { + let buttonSize = CGSize(width: 32.0, height: 44.0) + moreButton.setContent(.more(MoreHeaderButton.optionsCircleImage(color: .white))) + let moreFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - buttonSize.width) * 0.5), y: floorToScreenPixels((availableSize.height - buttonSize.height) * 0.5)), size: buttonSize) + transition.setPosition(view: moreButton.view, position: moreFrame.center) + transition.setBounds(view: moreButton.view, bounds: CGRect(origin: CGPoint(), size: moreFrame.size)) + + transition.setAlpha(view: moreButton.view, alpha: moreAlpha) + transition.setScale(view: moreButton.view, scale: moreAlpha == 0.0 ? 0.01 : 1.0) + } + if let micButton = self.micButton { if themeUpdated { micButton.updateTheme(theme: component.theme) @@ -325,7 +369,7 @@ public final class MessageInputActionButtonComponent: Component { if previousComponent?.mode != component.mode { switch component.mode { - case .none, .send, .apply, .voiceInput, .attach, .delete, .forward, .unavailableVoiceInput: + case .none, .send, .apply, .voiceInput, .attach, .delete, .forward, .unavailableVoiceInput, .more: micButton.updateMode(mode: .audio, animated: !transition.animation.isImmediate) case .videoInput: micButton.updateMode(mode: .video, animated: !transition.animation.isImmediate) diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 5220c19806..80381d742a 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -55,6 +55,7 @@ public final class MessageInputPanelComponent: Component { public let inputModeAction: (() -> Void)? public let timeoutAction: ((UIView) -> Void)? public let forwardAction: (() -> Void)? + public let moreAction: ((UIView, ContextGesture?) -> Void)? public let presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)? public let audioRecorder: ManagedAudioRecorder? public let videoRecordingStatus: InstantVideoControllerRecordingStatus? @@ -87,6 +88,7 @@ public final class MessageInputPanelComponent: Component { inputModeAction: (() -> Void)?, timeoutAction: ((UIView) -> Void)?, forwardAction: (() -> Void)?, + moreAction: ((UIView, ContextGesture?) -> Void)?, presentVoiceMessagesUnavailableTooltip: ((UIView) -> Void)?, audioRecorder: ManagedAudioRecorder?, videoRecordingStatus: InstantVideoControllerRecordingStatus?, @@ -118,6 +120,7 @@ public final class MessageInputPanelComponent: Component { self.inputModeAction = inputModeAction self.timeoutAction = timeoutAction self.forwardAction = forwardAction + self.moreAction = moreAction self.presentVoiceMessagesUnavailableTooltip = presentVoiceMessagesUnavailableTooltip self.audioRecorder = audioRecorder self.videoRecordingStatus = videoRecordingStatus @@ -189,6 +192,9 @@ public final class MessageInputPanelComponent: Component { if (lhs.forwardAction == nil) != (rhs.forwardAction == nil) { return false } + if (lhs.moreAction == nil) != (rhs.moreAction == nil) { + return false + } if lhs.hideKeyboard != rhs.hideKeyboard { return false } @@ -498,7 +504,11 @@ public final class MessageInputPanelComponent: Component { if component.attachmentAction != nil { let attachmentButtonMode: MessageInputActionButtonComponent.Mode - attachmentButtonMode = .attach + if !self.textFieldExternalState.isEditing && component.moreAction != nil { + attachmentButtonMode = .more + } else { + attachmentButtonMode = .attach + } let attachmentButtonSize = self.attachmentButton.update( transition: transition, @@ -526,6 +536,12 @@ public final class MessageInputPanelComponent: Component { }, stopAndPreviewMediaRecording: { }, + moreAction: { [weak self] view, gesture in + guard let self, let component = self.component else { + return + } + component.moreAction?(view, gesture) + }, context: component.context, theme: component.theme, strings: component.strings, @@ -709,6 +725,7 @@ public final class MessageInputPanelComponent: Component { } component.stopAndPreviewMediaRecording?() }, + moreAction: { _, _ in }, context: component.context, theme: component.theme, strings: component.strings, diff --git a/submodules/TelegramUI/Components/PeerReportScreen/BUILD b/submodules/TelegramUI/Components/PeerReportScreen/BUILD new file mode 100644 index 0000000000..95a0b40747 --- /dev/null +++ b/submodules/TelegramUI/Components/PeerReportScreen/BUILD @@ -0,0 +1,35 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "PeerReportScreen", + module_name = "PeerReportScreen", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/AsyncDisplayKit", + "//submodules/Display", + "//submodules/Postbox", + "//submodules/TelegramCore", + "//submodules/SSignalKit/SwiftSignalKit", + "//submodules/TelegramPresentationData", + "//submodules/AccountContext", + "//submodules/ContextUI", + "//submodules/UndoUI", + "//submodules/PresentationDataUtils", + "//submodules/AlertUI", + "//submodules/AppBundle", + "//submodules/TelegramUIPreferences", + "//submodules/TelegramPermissionsUI", + "//submodules/Markdown", + "//submodules/AnimatedStickerNode", + "//submodules/TelegramAnimatedStickerNode", + "//submodules/ShareController", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/PeerInfoUI/Sources/PeerReportController.swift b/submodules/TelegramUI/Components/PeerReportScreen/Sources/PeerReportScreen.swift similarity index 77% rename from submodules/PeerInfoUI/Sources/PeerReportController.swift rename to submodules/TelegramUI/Components/PeerReportScreen/Sources/PeerReportScreen.swift index 3e77918bc5..1e9b7fbbb4 100644 --- a/submodules/PeerInfoUI/Sources/PeerReportController.swift +++ b/submodules/TelegramUI/Components/PeerReportScreen/Sources/PeerReportScreen.swift @@ -19,6 +19,7 @@ public enum PeerReportSubject { case peer(EnginePeer.Id) case messages([EngineMessage.Id]) case profilePhoto(EnginePeer.Id, Int64) + case story(EnginePeer.Id, Int32) } public enum PeerReportOption { @@ -33,10 +34,34 @@ public enum PeerReportOption { case other } -public func presentPeerReportOptions(context: AccountContext, parent: ViewController, contextController: ContextControllerProtocol?, backAction: ((ContextControllerProtocol) -> Void)? = nil, subject: PeerReportSubject, options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .other], passthrough: Bool = false, completion: @escaping (ReportReason?, Bool) -> Void) { +public func presentPeerReportOptions( + context: AccountContext, + parent: ViewController, + contextController: ContextControllerProtocol?, + backAction: ((ContextControllerProtocol) -> Void)? = nil, + subject: PeerReportSubject, + options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .other], + passthrough: Bool = false, + forceTheme: PresentationTheme? = nil, + isDetailedReportingVisible: ((Bool) -> Void)? = nil, + completion: @escaping (ReportReason?, Bool) -> Void +) { if let contextController = contextController { - let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var presentationData = context.sharedContext.currentPresentationData.with { $0 } + if let forceTheme { + presentationData = presentationData.withUpdated(theme: forceTheme) + } var items: [ContextMenuItem] = [] + + if let _ = backAction { + items.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Back, icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) + }, iconPosition: .left, action: { (c, _) in + c.popItems() + }))) + items.append(.separator) + } + for option in options { let title: String let color: ContextMenuActionItemTextColor = .primary @@ -73,8 +98,6 @@ public func presentPeerReportOptions(context: AccountContext, parent: ViewContro items.append(.action(ContextMenuActionItem(text: title, textColor: color, icon: { theme in return generateTintedImage(image: icon, color: theme.contextMenu.primaryColor) }, action: { [weak parent] _, f in - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let reportReason: ReportReason switch option { case .spam: @@ -114,36 +137,46 @@ public func presentPeerReportOptions(context: AccountContext, parent: ViewContro completion(reportReason, true) } else { switch subject { - case let .peer(peerId): - let _ = (context.engine.peers.reportPeer(peerId: peerId, reason: reportReason, message: "") - |> deliverOnMainQueue).start(completed: { - displaySuccess() - completion(nil, false) - }) - case let .messages(messageIds): - let _ = (context.engine.peers.reportPeerMessages(messageIds: messageIds, reason: reportReason, message: "") - |> deliverOnMainQueue).start(completed: { - displaySuccess() - completion(nil, false) - }) - case let .profilePhoto(peerId, _): - let _ = (context.engine.peers.reportPeerPhoto(peerId: peerId, reason: reportReason, message: "") - |> deliverOnMainQueue).start(completed: { - displaySuccess() - completion(nil, false) - }) + case let .peer(peerId): + let _ = (context.engine.peers.reportPeer(peerId: peerId, reason: reportReason, message: "") + |> deliverOnMainQueue).start(completed: { + displaySuccess() + completion(nil, false) + }) + case let .messages(messageIds): + let _ = (context.engine.peers.reportPeerMessages(messageIds: messageIds, reason: reportReason, message: "") + |> deliverOnMainQueue).start(completed: { + displaySuccess() + completion(nil, false) + }) + case let .profilePhoto(peerId, _): + let _ = (context.engine.peers.reportPeerPhoto(peerId: peerId, reason: reportReason, message: "") + |> deliverOnMainQueue).start(completed: { + displaySuccess() + completion(nil, false) + }) + case let .story(peerId, storyId): + let _ = (context.engine.peers.reportPeerStory(peerId: peerId, storyId: storyId, reason: reportReason, message: "") + |> deliverOnMainQueue).start(completed: { + displaySuccess() + completion(nil, false) + }) } } } + isDetailedReportingVisible?(true) let controller = ActionSheetController(presentationData: presentationData, allowInputInset: true) + controller.dismissed = { _ in + isDetailedReportingVisible?(false) + } let dismissAction: () -> Void = { [weak controller] in controller?.dismissAnimated() } var message = "" var items: [ActionSheetItem] = [] items.append(ReportPeerHeaderActionSheetItem(context: context, text: presentationData.strings.Report_AdditionalDetailsText)) - items.append(ReportPeerDetailsActionSheetItem(context: context, placeholderText: presentationData.strings.Report_AdditionalDetailsPlaceholder, textUpdated: { text in + items.append(ReportPeerDetailsActionSheetItem(context: context, theme: presentationData.theme, placeholderText: presentationData.strings.Report_AdditionalDetailsPlaceholder, textUpdated: { text in message = text })) items.append(ActionSheetButtonItem(title: presentationData.strings.Report_Report, color: .accent, font: .bold, enabled: true, action: { @@ -154,22 +187,16 @@ public func presentPeerReportOptions(context: AccountContext, parent: ViewContro controller.setItemGroups([ ActionSheetItemGroup(items: items), - ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })]) + ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { + dismissAction() + })]) ]) parent?.present(controller, in: .window(.root)) } f(.dismissWithoutContent) }))) } - if let backAction = backAction { - items.append(.separator) - items.append(.action(ContextMenuActionItem(text: presentationData.strings.Common_Back, icon: { theme in - return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Back"), color: theme.actionSheet.primaryTextColor) - }, iconPosition: .left, action: { (c, _) in - backAction(c) - }))) - } - contextController.setItems(.single(ContextController.Items(content: .list(items))), minHeight: nil) + contextController.pushItems(items: .single(ContextController.Items(content: .list(items)))) } else { contextController?.dismiss(completion: nil) parent.view.endEditing(true) @@ -246,24 +273,30 @@ public func peerReportOptionsController(context: AccountContext, subject: PeerRe completion(reportReason, true) } else { switch subject { - case let .peer(peerId): - let _ = (context.engine.peers.reportPeer(peerId: peerId, reason: reportReason, message: message) - |> deliverOnMainQueue).start(completed: { - displaySuccess() - completion(nil, true) - }) - case let .messages(messageIds): - let _ = (context.engine.peers.reportPeerMessages(messageIds: messageIds, reason: reportReason, message: message) - |> deliverOnMainQueue).start(completed: { - displaySuccess() - completion(nil, true) - }) - case let .profilePhoto(peerId, _): - let _ = (context.engine.peers.reportPeerPhoto(peerId: peerId, reason: reportReason, message: message) - |> deliverOnMainQueue).start(completed: { - displaySuccess() - completion(nil, true) - }) + case let .peer(peerId): + let _ = (context.engine.peers.reportPeer(peerId: peerId, reason: reportReason, message: message) + |> deliverOnMainQueue).start(completed: { + displaySuccess() + completion(nil, true) + }) + case let .messages(messageIds): + let _ = (context.engine.peers.reportPeerMessages(messageIds: messageIds, reason: reportReason, message: message) + |> deliverOnMainQueue).start(completed: { + displaySuccess() + completion(nil, true) + }) + case let .profilePhoto(peerId, _): + let _ = (context.engine.peers.reportPeerPhoto(peerId: peerId, reason: reportReason, message: message) + |> deliverOnMainQueue).start(completed: { + displaySuccess() + completion(nil, true) + }) + case let .story(peerId, storyId): + let _ = (context.engine.peers.reportPeerStory(peerId: peerId, storyId: storyId, reason: reportReason, message: message) + |> deliverOnMainQueue).start(completed: { + displaySuccess() + completion(nil, true) + }) } } } @@ -276,7 +309,7 @@ public func peerReportOptionsController(context: AccountContext, subject: PeerRe var message = "" var items: [ActionSheetItem] = [] items.append(ReportPeerHeaderActionSheetItem(context: context, text: presentationData.strings.Report_AdditionalDetailsText)) - items.append(ReportPeerDetailsActionSheetItem(context: context, placeholderText: presentationData.strings.Report_AdditionalDetailsPlaceholder, textUpdated: { text in + items.append(ReportPeerDetailsActionSheetItem(context: context, theme: presentationData.theme, placeholderText: presentationData.strings.Report_AdditionalDetailsPlaceholder, textUpdated: { text in message = text })) items.append(ActionSheetButtonItem(title: presentationData.strings.Report_Report, color: .accent, font: .bold, enabled: true, action: { diff --git a/submodules/PeerInfoUI/Sources/ReportPeerDetailsActionSheetItem.swift b/submodules/TelegramUI/Components/PeerReportScreen/Sources/ReportPeerDetailsActionSheetItem.swift similarity index 78% rename from submodules/PeerInfoUI/Sources/ReportPeerDetailsActionSheetItem.swift rename to submodules/TelegramUI/Components/PeerReportScreen/Sources/ReportPeerDetailsActionSheetItem.swift index 654891e3bc..df156e2709 100644 --- a/submodules/PeerInfoUI/Sources/ReportPeerDetailsActionSheetItem.swift +++ b/submodules/TelegramUI/Components/PeerReportScreen/Sources/ReportPeerDetailsActionSheetItem.swift @@ -10,17 +10,19 @@ import AppBundle public final class ReportPeerDetailsActionSheetItem: ActionSheetItem { let context: AccountContext + let theme: PresentationTheme let placeholderText: String let textUpdated: (String) -> Void - public init(context: AccountContext, placeholderText: String, textUpdated: @escaping (String) -> Void) { + public init(context: AccountContext, theme: PresentationTheme, placeholderText: String, textUpdated: @escaping (String) -> Void) { self.context = context + self.theme = theme self.placeholderText = placeholderText self.textUpdated = textUpdated } public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode { - return ReportPeerDetailsActionSheetItemNode(theme: theme, context: self.context, placeholderText: self.placeholderText, textUpdated: self.textUpdated) + return ReportPeerDetailsActionSheetItemNode(theme: theme, presentationTheme: self.theme, context: self.context, placeholderText: self.placeholderText, textUpdated: self.textUpdated) } public func updateNode(_ node: ActionSheetItemNode) { @@ -34,11 +36,10 @@ private final class ReportPeerDetailsActionSheetItemNode: ActionSheetItemNode { private let accessibilityArea: AccessibilityAreaNode - init(theme: ActionSheetControllerTheme, context: AccountContext, placeholderText: String, textUpdated: @escaping (String) -> Void) { + init(theme: ActionSheetControllerTheme, presentationTheme: PresentationTheme, context: AccountContext, placeholderText: String, textUpdated: @escaping (String) -> Void) { self.theme = theme - let presentationData = context.sharedContext.currentPresentationData.with { $0 } - self.inputFieldNode = ShareInputFieldNode(theme: ShareInputFieldNodeTheme(presentationTheme: presentationData.theme), placeholder: placeholderText) + self.inputFieldNode = ShareInputFieldNode(theme: ShareInputFieldNodeTheme(presentationTheme: presentationTheme), placeholder: placeholderText) self.accessibilityArea = AccessibilityAreaNode() diff --git a/submodules/PeerInfoUI/Sources/ReportPeerHeaderActionSheetItem.swift b/submodules/TelegramUI/Components/PeerReportScreen/Sources/ReportPeerHeaderActionSheetItem.swift similarity index 100% rename from submodules/PeerInfoUI/Sources/ReportPeerHeaderActionSheetItem.swift rename to submodules/TelegramUI/Components/PeerReportScreen/Sources/ReportPeerHeaderActionSheetItem.swift diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD index 173e7928e6..e3cc085688 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/BUILD @@ -64,6 +64,7 @@ swift_library( "//submodules/UrlEscaping", "//submodules/OverlayStatusController", "//submodules/Utils/VolumeButtons", + "//submodules/TelegramUI/Components/PeerReportScreen", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift index 895feded21..e49dfa2213 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryContent.swift @@ -60,7 +60,7 @@ public final class StoryContentItem { } public let id: AnyHashable - public let position: Int + public let position: Int? public let component: AnyComponent public let centerInfoComponent: AnyComponent? public let rightInfoComponent: AnyComponent? @@ -70,7 +70,7 @@ public final class StoryContentItem { public init( id: AnyHashable, - position: Int, + position: Int?, component: AnyComponent, centerInfoComponent: AnyComponent?, rightInfoComponent: AnyComponent?, @@ -120,12 +120,17 @@ public final class StoryContentItemSlice { public final class StoryContentContextState { public final class AdditionalPeerData: Equatable { public static func == (lhs: StoryContentContextState.AdditionalPeerData, rhs: StoryContentContextState.AdditionalPeerData) -> Bool { - return lhs.areVoiceMessagesAvailable == rhs.areVoiceMessagesAvailable + return lhs.isMuted == rhs.isMuted && lhs.areVoiceMessagesAvailable == rhs.areVoiceMessagesAvailable } + public let isMuted: Bool public let areVoiceMessagesAvailable: Bool - public init(areVoiceMessagesAvailable: Bool) { + public init( + isMuted: Bool, + areVoiceMessagesAvailable: Bool + ) { + self.isMuted = isMuted self.areVoiceMessagesAvailable = areVoiceMessagesAvailable } } diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift index a78cd69935..395ce11edc 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerComponent.swift @@ -22,6 +22,7 @@ import ShareWithPeersScreen import PlainButtonComponent import TooltipUI import PresentationDataUtils +import PeerReportScreen public final class StoryItemSetContainerComponent: Component { public final class ExternalState { @@ -265,6 +266,8 @@ public final class StoryItemSetContainerComponent: Component { weak var contextController: ContextController? weak var privacyController: ShareWithPeersScreen? + var isReporting: Bool = false + var component: StoryItemSetContainerComponent? weak var state: EmptyComponentState? @@ -552,6 +555,9 @@ public final class StoryItemSetContainerComponent: Component { if self.privacyController != nil { return true } + if self.isReporting { + return true + } if self.isEditingStory { return true } @@ -1167,6 +1173,95 @@ public final class StoryItemSetContainerComponent: Component { } self.sendMessageContext.performShareAction(view: self) } : nil, + moreAction: { [weak self] sourceView, gesture in + guard let self, let component = self.component, let controller = component.controller() else { + return + } + + component.controller()?.forEachController { c in + if let c = c as? UndoOverlayController { + c.dismiss() + } + return true + } + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: component.theme) + var items: [ContextMenuItem] = [] + items.append(.action(ContextMenuActionItem(text: component.slice.additionalPeerData.isMuted ? "Notify" : "Not Notify", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: component.slice.additionalPeerData.isMuted ? "Chat/Context Menu/Unmute" : "Chat/Context Menu/Muted"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self, let component = self.component else { + return + } + + let _ = component.context.engine.peers.togglePeerStoriesMuted(peerId: component.slice.peer.id).start() + }))) + + var isHidden = false + if case let .user(user) = component.slice.peer, let storiesHidden = user.storiesHidden { + isHidden = storiesHidden + } + + items.append(.action(ContextMenuActionItem(text: isHidden ? "Unhide \(component.slice.peer.compactDisplayTitle)" : "Hide \(component.slice.peer.compactDisplayTitle)", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: component.slice.item.storyItem.isPinned ? "Chat/Context Menu/Archive" : "Chat/Context Menu/Archive"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] _, a in + a(.default) + + guard let self, let component = self.component else { + return + } + + let _ = component.context.engine.peers.updatePeerStoriesHidden(id: component.slice.peer.id, isHidden: true) + }))) + + items.append(.action(ContextMenuActionItem(text: "Report", icon: { theme in + return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Report"), color: theme.contextMenu.primaryColor) + }, action: { [weak self] c, a in + guard let self, let component = self.component, let controller = component.controller() else { + return + } + + let options: [PeerReportOption] = [.spam, .violence, .pornography, .childAbuse, .copyright, .illegalDrugs, .personalDetails, .other] + presentPeerReportOptions( + context: component.context, + parent: controller, + contextController: c, + backAction: { _ in }, + subject: .story(component.slice.peer.id, component.slice.item.storyItem.id), + options: options, + passthrough: true, + forceTheme: defaultDarkPresentationTheme, + isDetailedReportingVisible: { [weak self] isReporting in + guard let self else { + return + } + self.isReporting = isReporting + self.updateIsProgressPaused() + }, + completion: { [weak self] reason, _ in + guard let self, let component = self.component, let controller = component.controller(), let reason else { + return + } + let _ = component.context.engine.peers.reportPeerStory(peerId: component.slice.peer.id, storyId: component.slice.item.storyItem.id, reason: reason, message: "").start() + controller.present(UndoOverlayController(presentationData: presentationData, content: .emoji(name: "PoliceCar", text: presentationData.strings.Report_Succeed), elevatedLayout: false, action: { _ in return false }), in: .current) + } + ) + }))) + + let contextController = ContextController(account: component.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: gesture) + contextController.dismissed = { [weak self] in + guard let self else { + return + } + self.contextController = nil + self.updateIsProgressPaused() + } + self.contextController = contextController + self.updateIsProgressPaused() + controller.present(contextController, in: .window(.root)) + }, presentVoiceMessagesUnavailableTooltip: { [weak self] view in guard let self, let component = self.component, self.voiceMessagesRestrictedTooltipController == nil else { return @@ -1337,6 +1432,13 @@ public final class StoryItemSetContainerComponent: Component { return } + component.controller()?.forEachController { c in + if let c = c as? UndoOverlayController { + c.dismiss() + } + return true + } + var items: [ContextMenuItem] = [] let additionalCount = component.slice.item.storyItem.privacy?.additionallyIncludePeers.count ?? 0 @@ -1392,14 +1494,7 @@ public final class StoryItemSetContainerComponent: Component { }))) items.append(.separator) - - component.controller()?.forEachController { c in - if let c = c as? UndoOverlayController { - c.dismiss() - } - return true - } - + items.append(.action(ContextMenuActionItem(text: component.slice.item.storyItem.isPinned ? "Remove from profile" : "Save to profile", icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: component.slice.item.storyItem.isPinned ? "Chat/Context Menu/Check" : "Chat/Context Menu/Add"), color: theme.contextMenu.primaryColor) }, action: { [weak self] _, a in @@ -1879,7 +1974,7 @@ public final class StoryItemSetContainerComponent: Component { self.addSubview(targetView) reactionContextNode.willAnimateOutToReaction(value: updateReaction.reaction) - reactionContextNode.animateOutToReaction(value: updateReaction.reaction, targetView: targetView, hideNode: false, animateTargetContainer: nil, addStandaloneReactionAnimation: { [weak self] standaloneReactionAnimation in + reactionContextNode.animateOutToReaction(value: updateReaction.reaction, targetView: targetView, hideNode: false, animateTargetContainer: nil, addStandaloneReactionAnimation: "".isEmpty ? nil : { [weak self] standaloneReactionAnimation in guard let self else { return } @@ -1974,7 +2069,7 @@ public final class StoryItemSetContainerComponent: Component { } } else { reactionContextNodeTransition.setFrame(view: reactionContextNode.view, frame: CGRect(origin: CGPoint(), size: availableSize)) - reactionContextNode.updateLayout(size: availableSize, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, isCoveredByInput: false, isAnimatingOut: false, transition: reactionContextNodeTransition.containedViewLayoutTransition) + reactionContextNode.updateLayout(size: availableSize, insets: UIEdgeInsets(), anchorRect: reactionsAnchorRect, centerAligned: true, isCoveredByInput: false, isAnimatingOut: false, transition: reactionContextNodeTransition.containedViewLayoutTransition) if animateReactionsIn { reactionContextNode.animateIn(from: reactionsAnchorRect) @@ -2041,12 +2136,10 @@ public final class StoryItemSetContainerComponent: Component { self.ignoreScrolling = false self.updateScrolling(transition: transition) - if let focusedItem, let visibleItem = self.visibleItems[focusedItem.storyItem.id] { + if let focusedItem, let visibleItem = self.visibleItems[focusedItem.storyItem.id], let index = focusedItem.position { let navigationStripSideInset: CGFloat = 8.0 let navigationStripTopInset: CGFloat = 8.0 - let index = focusedItem.position - let _ = self.navigationStrip.update( transition: transition, component: AnyComponent(MediaNavigationStripComponent( diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift index 6eb6b8c203..aaa422da7b 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryChatContent.swift @@ -51,7 +51,7 @@ public final class StoryContentContextImpl: StoryContentContext { PostboxViewKey.basicPeer(peerId), PostboxViewKey.cachedPeerData(peerId: peerId), PostboxViewKey.storiesState(key: .peer(peerId)), - PostboxViewKey.storyItems(peerId: peerId) + PostboxViewKey.storyItems(peerId: peerId), ] if peerId == context.account.peerId { inputKeys.append(PostboxViewKey.storiesState(key: .local)) @@ -60,10 +60,11 @@ public final class StoryContentContextImpl: StoryContentContext { self.currentFocusedIdUpdatedPromise.get(), context.account.postbox.combinedView( keys: inputKeys - ) + ), + context.engine.data.subscribe(TelegramEngine.EngineData.Item.NotificationSettings.Global()) ) - |> mapToSignal { _, views -> Signal<(CombinedView, [PeerId: Peer]), NoError> in - return context.account.postbox.transaction { transaction -> (CombinedView, [PeerId: Peer]) in + |> mapToSignal { _, views, globalNotificationSettings -> Signal<(CombinedView, [PeerId: Peer], EngineGlobalNotificationSettings), NoError> in + return context.account.postbox.transaction { transaction -> (CombinedView, [PeerId: Peer], EngineGlobalNotificationSettings) in var peers: [PeerId: Peer] = [:] if let itemsView = views.views[PostboxViewKey.storyItems(peerId: peerId)] as? StoryItemsView { for item in itemsView.items { @@ -78,10 +79,10 @@ public final class StoryContentContextImpl: StoryContentContext { } } } - return (views, peers) + return (views, peers, globalNotificationSettings) } } - |> deliverOnMainQueue).start(next: { [weak self] views, peers in + |> deliverOnMainQueue).start(next: { [weak self] views, peers, globalNotificationSettings in guard let self else { return } @@ -99,10 +100,15 @@ public final class StoryContentContextImpl: StoryContentContext { } let additionalPeerData: StoryContentContextState.AdditionalPeerData if let cachedPeerDataView = views.views[PostboxViewKey.cachedPeerData(peerId: peerId)] as? CachedPeerDataView, let cachedUserData = cachedPeerDataView.cachedPeerData as? CachedUserData { - let _ = cachedUserData - additionalPeerData = StoryContentContextState.AdditionalPeerData(areVoiceMessagesAvailable: false) //cachedUserData.voiceMessagesAvailable) + var isMuted = false + if let notificationSettings = peerView.notificationSettings as? TelegramPeerNotificationSettings, let storiesMuted = notificationSettings.storiesMuted { + isMuted = storiesMuted + } else { + isMuted = globalNotificationSettings.privateChats.storiesMuted + } + additionalPeerData = StoryContentContextState.AdditionalPeerData(isMuted: isMuted, areVoiceMessagesAvailable: cachedUserData.voiceMessagesAvailable) } else { - additionalPeerData = StoryContentContextState.AdditionalPeerData(areVoiceMessagesAvailable: true) + additionalPeerData = StoryContentContextState.AdditionalPeerData(isMuted: true, areVoiceMessagesAvailable: true) } let state = stateView.value?.get(Stories.PeerState.self) @@ -874,7 +880,9 @@ public final class SingleStoryContentContextImpl: StoryContentContext { self.storyDisposable = (combineLatest(queue: .mainQueue(), context.engine.data.subscribe( TelegramEngine.EngineData.Item.Peer.Peer(id: storyId.peerId), - TelegramEngine.EngineData.Item.Peer.AreVoiceMessagesAvailable(id: storyId.peerId) + TelegramEngine.EngineData.Item.Peer.AreVoiceMessagesAvailable(id: storyId.peerId), + TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: storyId.peerId), + TelegramEngine.EngineData.Item.NotificationSettings.Global() ), context.account.postbox.transaction { transaction -> (Stories.StoredItem?, [PeerId: Peer]) in guard let item = transaction.getStory(id: storyId)?.get(Stories.StoredItem.self) else { @@ -893,15 +901,23 @@ public final class SingleStoryContentContextImpl: StoryContentContext { return (item, peers) } ) - |> deliverOnMainQueue).start(next: { [weak self] peerAndVoiceMessages, itemAndPeers in + |> deliverOnMainQueue).start(next: { [weak self] data, itemAndPeers in guard let self else { return } - let (peer, areVoiceMessagesAvailable) = peerAndVoiceMessages + let (peer, areVoiceMessagesAvailable, notificationSettings, globalNotificationSettings) = data let (item, peers) = itemAndPeers + var isMuted = false + if let storiesMuted = notificationSettings.storiesMuted { + isMuted = storiesMuted + } else { + isMuted = globalNotificationSettings.privateChats.storiesMuted + } + let additionalPeerData = StoryContentContextState.AdditionalPeerData( + isMuted: isMuted, areVoiceMessagesAvailable: areVoiceMessagesAvailable ) @@ -1039,19 +1055,30 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { self.storyDisposable = (combineLatest(queue: .mainQueue(), context.engine.data.subscribe( TelegramEngine.EngineData.Item.Peer.Peer(id: peerId), - TelegramEngine.EngineData.Item.Peer.AreVoiceMessagesAvailable(id: peerId) + TelegramEngine.EngineData.Item.Peer.AreVoiceMessagesAvailable(id: peerId), + TelegramEngine.EngineData.Item.Peer.NotificationSettings(id: peerId), + TelegramEngine.EngineData.Item.NotificationSettings.Global() ), listContext.state, self.focusedIdUpdated.get() ) //|> delay(0.4, queue: .mainQueue()) - |> deliverOnMainQueue).start(next: { [weak self] peerAndVoiceMessages, state, _ in + |> deliverOnMainQueue).start(next: { [weak self] data, state, _ in guard let self else { return } - let (peer, areVoiceMessagesAvailable) = peerAndVoiceMessages + let (peer, areVoiceMessagesAvailable, notificationSettings, globalNotificationSettings) = data + + var isMuted = false + if let storiesMuted = notificationSettings.storiesMuted { + isMuted = storiesMuted + } else { + isMuted = globalNotificationSettings.privateChats.storiesMuted + } + let additionalPeerData = StoryContentContextState.AdditionalPeerData( + isMuted: isMuted, areVoiceMessagesAvailable: areVoiceMessagesAvailable ) @@ -1095,16 +1122,16 @@ public final class PeerStoryListContentContextImpl: StoryContentContext { additionalPeerData: additionalPeerData, item: StoryContentItem( id: AnyHashable(item.id), - position: focusedIndex, + position: nil, component: AnyComponent(StoryItemContentComponent( context: context, peer: peer, item: item )), - centerInfoComponent: AnyComponent(StoryAuthorInfoComponent( + centerInfoComponent: AnyComponent(StoryPositionInfoComponent( context: context, - peer: peer, - timestamp: item.timestamp + position: focusedIndex, + totalCount: state.totalCount )), rightInfoComponent: AnyComponent(StoryAvatarInfoComponent( context: context, diff --git a/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryPositionInfoComponent.swift b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryPositionInfoComponent.swift new file mode 100644 index 0000000000..6cf01b4e34 --- /dev/null +++ b/submodules/TelegramUI/Components/Stories/StoryContentComponent/Sources/StoryPositionInfoComponent.swift @@ -0,0 +1,88 @@ +import Foundation +import UIKit +import Display +import ComponentFlow +import AccountContext +import TelegramCore +import TelegramStringFormatting + +final class StoryPositionInfoComponent: Component { + let context: AccountContext + let position: Int + let totalCount: Int + + init(context: AccountContext, position: Int, totalCount: Int) { + self.context = context + self.position = position + self.totalCount = totalCount + } + + static func ==(lhs: StoryPositionInfoComponent, rhs: StoryPositionInfoComponent) -> Bool { + if lhs.context !== rhs.context { + return false + } + if lhs.position != rhs.position { + return false + } + if lhs.totalCount != rhs.totalCount { + return false + } + return true + } + + final class View: UIView { + private let title = ComponentView() + + private var component: StoryPositionInfoComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + + self.isUserInteractionEnabled = false + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(component: StoryPositionInfoComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + let size = availableSize + + let presentationData = component.context.sharedContext.currentPresentationData.with({ $0 }) + + let position = max(0, min(component.position + 1, component.totalCount)) + let title = presentationData.strings.Items_NOfM("\(position)", "\(component.totalCount)").string + let titleSize = self.title.update( + transition: .immediate, + component: AnyComponent(Text(text: title, font: Font.with(size: 17.0, weight: .semibold, traits: .monospacedNumbers), color: .white)), + environment: {}, + containerSize: availableSize + ) + + let contentHeight: CGFloat = titleSize.height + let titleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - titleSize.width) * 0.5), y: floor((availableSize.height - contentHeight) * 0.5)), size: titleSize) + + if let titleView = self.title.view { + if titleView.superview == nil { + titleView.isUserInteractionEnabled = false + self.addSubview(titleView) + } + transition.setFrame(view: titleView, frame: titleFrame) + } + + return size + } + } + + func makeView() -> View { + return View(frame: CGRect()) + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index b3d72a7f22..91b9eefa01 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -100,6 +100,7 @@ import MoreHeaderButton import VolumeButtons import ChatAvatarNavigationNode import ChatContextQuery +import PeerReportScreen #if DEBUG import os.signpost @@ -8249,7 +8250,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G var message = "" var items: [ActionSheetItem] = [] items.append(ReportPeerHeaderActionSheetItem(context: strongSelf.context, text: presentationData.strings.Report_AdditionalDetailsText)) - items.append(ReportPeerDetailsActionSheetItem(context: strongSelf.context, placeholderText: presentationData.strings.Report_AdditionalDetailsPlaceholder, textUpdated: { text in + items.append(ReportPeerDetailsActionSheetItem(context: strongSelf.context, theme: presentationData.theme, placeholderText: presentationData.strings.Report_AdditionalDetailsPlaceholder, textUpdated: { text in message = text })) items.append(ActionSheetButtonItem(title: presentationData.strings.Report_Report, color: .accent, font: .bold, enabled: true, action: { diff --git a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift index 0ac56e37fe..7e602158d9 100644 --- a/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Sources/PeerInfo/PeerInfoScreen.swift @@ -91,6 +91,7 @@ import PeerInfoStoryGridScreen import StoryContainerScreen import StoryContentComponent import ChatAvatarNavigationNode +import PeerReportScreen enum PeerInfoAvatarEditingMode { case generic diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index e502b41523..481470f812 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1768,6 +1768,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSource = .fasterDownload case .translation: mappedSource = .translation + case .stories: + mappedSource = .stories } return PremiumIntroScreen(context: context, source: mappedSource) } @@ -1803,6 +1805,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { mappedSubject = .emojiStatus case .translation: mappedSubject = .translation + case .stories: + mappedSubject = .stories } return PremiumDemoScreen(context: context, subject: mappedSubject, action: action) } diff --git a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift index 38d60a723d..a9ec128ff1 100644 --- a/submodules/TextFormat/Sources/ChatTextInputAttributes.swift +++ b/submodules/TextFormat/Sources/ChatTextInputAttributes.swift @@ -587,6 +587,7 @@ public func refreshChatTextInputAttributes(textView: UITextView, primaryTextColo } } else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute { textView.textStorage.addAttribute(key, value: value, range: range) + textView.textStorage.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.clear, range: range) } }