diff --git a/Telegram/Telegram-iOS/en.lproj/Localizable.strings b/Telegram/Telegram-iOS/en.lproj/Localizable.strings index 71e02d506c..73f827f844 100644 --- a/Telegram/Telegram-iOS/en.lproj/Localizable.strings +++ b/Telegram/Telegram-iOS/en.lproj/Localizable.strings @@ -16208,3 +16208,11 @@ Error: %8$@"; "Settings.Birthday.PrivacyHelpEveryone" = "Everyone can see your birthday. [Change >]()"; "Settings.Birthday.PrivacyHelpContacts" = "Only your contacts can see your birthday. [Change >]()"; "Settings.Birthday.PrivacyHelpNobody" = "Nobody can see your birthday. [Change >]()"; + +"GroupPermission.NoSendReactions" = "no reactions"; +"Channel.BanUser.PermissionSendReactions" = "Send Reactions"; + +"Chat.AdminAction.ToastReactionsDeletedTitleSingle" = "Reaction Deleted"; +"Chat.AdminAction.ToastReactionsDeletedTextSingle" = "Reaction Deleted."; +"Chat.AdminAction.ToastReactionsDeletedTextMultiple" = "Messages Deleted."; +"Chat.AdminAction.ToastMessagesAndReactionsDeletedText" = "Messages and reactions deleted."; diff --git a/submodules/AccountContext/Sources/ContactSelectionController.swift b/submodules/AccountContext/Sources/ContactSelectionController.swift index 54d5cb44ad..4b256510b1 100644 --- a/submodules/AccountContext/Sources/ContactSelectionController.swift +++ b/submodules/AccountContext/Sources/ContactSelectionController.swift @@ -9,7 +9,7 @@ public protocol ContactSelectionController: ViewController { var result: Signal<([ContactListPeer], ContactListAction, Bool, Int32?, NSAttributedString?, ChatSendMessageActionSheetController.SendParameters?)?, NoError> { get } var displayProgress: Bool { get set } var dismissed: (() -> Void)? { get set } - var presentScheduleTimePicker: (@escaping (Int32, Int32?) -> Void) -> Void { get set } + var presentScheduleTimePicker: (@escaping (Int32, Int32?, Bool) -> Void) -> Void { get set } func dismissSearch() } diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift index e5fc76b8b4..313d21980a 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequenceController.swift @@ -997,7 +997,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth controller.reset = { [weak self, weak controller] in if let strongSelf = self, let strongController = controller { strongController.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: suggestReset ? strongSelf.presentationData.strings.TwoStepAuth_RecoveryFailed : strongSelf.presentationData.strings.TwoStepAuth_RecoveryUnavailable, actions: [ - TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Login_ResetAccountProtected_Reset, action: { if let strongSelf = self, let strongController = controller { strongController.inProgress = true @@ -1084,7 +1084,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth controller.reset = { [weak self, weak controller] in if let strongSelf = self, let strongController = controller { strongController.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_ResetAccountConfirmation, actions: [ - TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Login_ResetAccountProtected_Reset, action: { if let strongSelf = self, let strongController = controller { strongController.inProgress = true diff --git a/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift b/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift index 20f70b12ce..07fe4c61f9 100644 --- a/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift +++ b/submodules/AuthorizationUI/Sources/AuthorizationSequencePaymentScreen.swift @@ -168,7 +168,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component { title: nil, text: errorText, actions: [ - TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}), + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Login_PhoneNumberHelp, action: { [weak self] in guard let self else { return diff --git a/submodules/BrowserUI/BUILD b/submodules/BrowserUI/BUILD index 74cd9f60b2..8b9315a112 100644 --- a/submodules/BrowserUI/BUILD +++ b/submodules/BrowserUI/BUILD @@ -31,6 +31,7 @@ swift_library( "//submodules/TelegramUI/Components/MinimizedContainer", "//submodules/Pasteboard", "//submodules/SaveToCameraRoll", + "//submodules/TextFormat:TextFormat", "//submodules/TelegramUI/Components/NavigationStackComponent", "//submodules/LocationUI", "//submodules/OpenInExternalAppUI", diff --git a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift index 4f9c55b877..55ccbbbf00 100644 --- a/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift +++ b/submodules/BrowserUI/Sources/BrowserInstantPageContent.swift @@ -20,6 +20,7 @@ import SafariServices import LocationUI import OpenInExternalAppUI import GalleryUI +import TextFormat final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDelegate { private let context: AccountContext @@ -67,6 +68,8 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg var currentDetailsItems: [InstantPageDetailsItem] = [] private var resolvedExternalMediaDimensions: [MediaId: PixelDimensions] = [:] private var pendingResolvedExternalMediaDimensions = Set() + private var codeHighlight: CachedMessageSyntaxHighlight? + private var codeHighlightState: (specs: [CachedMessageSyntaxHighlight.Spec], disposable: Disposable)? var currentAccessibilityAreas: [AccessibilityAreaNode] = [] @@ -90,6 +93,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg private let resolveUrlDisposable = MetaDisposable() private let updateLayoutDisposable = MetaDisposable() private let updateExternalMediaDimensionsDisposable = MetaDisposable() + private let updateCodeHighlightDisposable = MetaDisposable() private let loadProgress = ValuePromise(1.0, ignoreRepeated: true) private let readingProgress = ValuePromise(0.0, ignoreRepeated: true) @@ -190,6 +194,8 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg self.updateWebPage(result, anchor: self.initialAnchor) }) } + + self.updateCodeHighlight() } deinit { @@ -199,6 +205,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg self.resolveUrlDisposable.dispose() self.updateLayoutDisposable.dispose() self.updateExternalMediaDimensionsDisposable.dispose() + self.updateCodeHighlightDisposable.dispose() } required init?(coder: NSCoder) { @@ -325,6 +332,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg } } self.currentLayout = nil + self.updateCodeHighlight() self.updatePageLayout() self.scrollNode.frame = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0) @@ -490,7 +498,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg return } - let currentLayout = instantPageLayoutForWebPage(webPage, instantPage: instantPage, userLocation: self.sourceLocation.userLocation, boundingWidth: size.width, safeInset: insets.left, strings: self.presentationData.strings, theme: self.theme, dateTimeFormat: self.presentationData.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights) + let currentLayout = instantPageLayoutForWebPage(webPage, instantPage: instantPage, userLocation: self.sourceLocation.userLocation, boundingWidth: size.width, safeInset: insets.left, strings: self.presentationData.strings, theme: self.theme, dateTimeFormat: self.presentationData.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights, cachedMessageSyntaxHighlight: self.codeHighlight) let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: size.width) @@ -551,6 +559,96 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg self.scrollNode.view.contentSize = currentLayout.contentSize self.scrollNodeFooter.frame = CGRect(origin: CGPoint(x: 0.0, y: currentLayout.contentSize.height), size: CGSize(width: size.width, height: 2000.0)) } + + private func updateCodeHighlight() { + guard let instantPage = self.webPage?.instantPage else { + self.codeHighlight = nil + self.codeHighlightState = nil + self.updateCodeHighlightDisposable.set(nil) + return + } + + let specs = syntaxHighlightSpecs(for: instantPage.blocks) + if let currentState = self.codeHighlightState, currentState.specs == specs { + return + } + + if specs.isEmpty { + let hadHighlight = self.codeHighlight != nil + self.codeHighlight = nil + self.codeHighlightState = nil + self.updateCodeHighlightDisposable.set(nil) + if hadHighlight { + self.updatePageLayout() + self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds) + } + return + } + + let disposable = MetaDisposable() + self.codeHighlightState = (specs, disposable) + self.updateCodeHighlightDisposable.set(disposable) + disposable.set((asyncStanaloneSyntaxHighlight(current: self.codeHighlight, specs: specs) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + if self.codeHighlight != result { + self.codeHighlight = result + self.updatePageLayout() + self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds) + } + })) + } + + private func syntaxHighlightSpecs(for blocks: [InstantPageBlock]) -> [CachedMessageSyntaxHighlight.Spec] { + var specs: [CachedMessageSyntaxHighlight.Spec] = [] + var seen = Set() + + func collect(blocks: [InstantPageBlock]) { + for block in blocks { + switch block { + case let .preformatted(text, language): + guard let language = normalizedCodeBlockLanguage(language), !text.plainText.isEmpty else { + continue + } + let spec = CachedMessageSyntaxHighlight.Spec(language: language, text: text.plainText) + if seen.insert(spec).inserted { + specs.append(spec) + } + case let .cover(block): + collect(blocks: [block]) + case let .postEmbed(_, _, _, _, _, blocks, _): + collect(blocks: blocks) + case let .collage(items, _): + collect(blocks: items) + case let .slideshow(items, _): + collect(blocks: items) + case let .details(_, blocks, _): + collect(blocks: blocks) + case let .list(items, _): + for item in items { + if case let .blocks(blocks, _) = item { + collect(blocks: blocks) + } + } + default: + break + } + } + } + + collect(blocks: blocks) + return specs + } + + private func normalizedCodeBlockLanguage(_ language: String?) -> String? { + guard let language else { + return nil + } + let normalized = language.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + return normalized.isEmpty ? nil : normalized + } func updateVisibleItems(visibleBounds: CGRect, animated: Bool = false) { var visibleTileIndices = Set() diff --git a/submodules/BrowserUI/Sources/BrowserMarkdown.swift b/submodules/BrowserUI/Sources/BrowserMarkdown.swift index 3f09ec5cd4..debef1e807 100644 --- a/submodules/BrowserUI/Sources/BrowserMarkdown.swift +++ b/submodules/BrowserUI/Sources/BrowserMarkdown.swift @@ -330,7 +330,7 @@ private func markdownBlocks(from node: MarkdownIntentNode, context: MarkdownConv } else if level == 2 { return [.header(text)] } else { - return [.subheader(text)] + return [.heading(text: text, level: Int32(max(3, min(level, 6))))] } case .paragraph: let inlineContent = markdownInlineContent(from: node.attributedText, context: context) @@ -349,12 +349,12 @@ private func markdownBlocks(from node: MarkdownIntentNode, context: MarkdownConv return [] } return [.paragraph(text)] - case .codeBlock: + case let .codeBlock(languageHint): let text = markdownRichText(from: markdownTrimTrailingCodeBlockNewline(node.attributedText), context: context) guard markdownHasDisplayableContent(text) else { return [] } - return [.preformatted(text)] + return [.preformatted(text: text, language: markdownNormalizedCodeBlockLanguage(languageHint))] case .thematicBreak: return [.divider] case .blockQuote: @@ -897,9 +897,11 @@ private func markdownPlainText(from block: InstantPageBlock) -> String { return text.plainText case let .subheader(text): return text.plainText + case let .heading(text, _): + return text.plainText case let .paragraph(text): return text.plainText - case let .preformatted(text): + case let .preformatted(text, _): return text.plainText case let .footer(text): return text.plainText @@ -940,6 +942,14 @@ private func markdownTitle(from blocks: [InstantPageBlock], file: FileMediaRefer return fileURL.lastPathComponent } +private func markdownNormalizedCodeBlockLanguage(_ language: String?) -> String? { + guard let language else { + return nil + } + let normalized = language.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + return normalized.isEmpty ? nil : normalized +} + private func markdownFirstParagraphText(from blocks: [InstantPageBlock]) -> String? { for block in blocks { switch block { @@ -1007,6 +1017,8 @@ private func markdownHeadingText(from block: InstantPageBlock) -> String? { return text.plainText case let .subheader(text): return text.plainText + case let .heading(text, _): + return text.plainText default: return nil } diff --git a/submodules/BrowserUI/Sources/BrowserReadability.swift b/submodules/BrowserUI/Sources/BrowserReadability.swift index 4f734d98e4..3f5a45c894 100644 --- a/submodules/BrowserUI/Sources/BrowserReadability.swift +++ b/submodules/BrowserUI/Sources/BrowserReadability.swift @@ -747,10 +747,16 @@ private func parsePageBlocks(_ input: [Any], _ url: String, _ media: inout [Medi result.append(.paragraph(trim(parseRichText(item, &media)))) case "h1", "h2": result.append(.header(trim(parseRichText(item, &media)))) - case "h3", "h4", "h5", "h6": - result.append(.subheader(trim(parseRichText(item, &media)))) + case "h3": + result.append(.heading(text: trim(parseRichText(item, &media)), level: 3)) + case "h4": + result.append(.heading(text: trim(parseRichText(item, &media)), level: 4)) + case "h5": + result.append(.heading(text: trim(parseRichText(item, &media)), level: 5)) + case "h6": + result.append(.heading(text: trim(parseRichText(item, &media)), level: 6)) case "pre": - result.append(.preformatted(.fixed(trim(parseRichText(item, &media))))) + result.append(.preformatted(text: .fixed(trim(parseRichText(item, &media))), language: nil)) case "blockquote": result.append(.blockQuote(text: .italic(trim(parseRichText(item, &media))), caption: .empty)) case "img": diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 6e2cbd0c81..dcd30d5bb8 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -163,6 +163,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController return false } } + + private var foldersCount: Int32 { + guard let tabContainerData = self.tabContainerData else { + return 0 + } + return Int32(tabContainerData.0.count(where: { entry in + if case .filter = entry { + return true + } else { + return false + } + })) + } private var hasDownloads: Bool = false private var activeDownloadsDisposable: Disposable? @@ -1015,7 +1028,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if isDisabled { let context = self.context var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(self.tabContainerData?.0.count ?? 0), action: { + let controller = PremiumLimitScreen(context: context, subject: .folders, count: self.foldersCount, action: { let controller = PremiumIntroScreen(context: context, source: .folders) replaceImpl?(controller) return true @@ -1059,7 +1072,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if isDisabled { let context = self.context var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(self.tabContainerData?.0.count ?? 0), action: { + let controller = PremiumLimitScreen(context: context, subject: .folders, count: self.foldersCount, action: { let controller = PremiumIntroScreen(context: context, source: .folders) replaceImpl?(controller) return true @@ -2377,7 +2390,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } let context = strongSelf.context var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(strongSelf.tabContainerData?.0.count ?? 0), action: { + let controller = PremiumLimitScreen(context: context, subject: .folders, count: strongSelf.foldersCount, action: { let controller = PremiumIntroScreen(context: context, source: .folders) replaceImpl?(controller) return true @@ -4563,7 +4576,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: { confirmDeleteFolder() }), - TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: { + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { }) ]), in: .window(.root)) } else { @@ -6251,7 +6264,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if isDisabled { let context = strongSelf.context var replaceImpl: ((ViewController) -> Void)? - let controller = PremiumLimitScreen(context: context, subject: .folders, count: Int32(strongSelf.tabContainerData?.0.count ?? 0), action: { + let controller = PremiumLimitScreen(context: context, subject: .folders, count: strongSelf.foldersCount, action: { let controller = PremiumIntroScreen(context: context, source: .folders) replaceImpl?(controller) return true diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index a036e41e04..826aa80f86 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1504,6 +1504,24 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { if self.controller?.tabContainerData != nil || !panels.isEmpty { var tabs: AnyComponent? if let tabContainerData = self.controller?.tabContainerData, tabContainerData.0.count > 1 { + let folderFilterIndex: (ChatListFilterTabEntryId, [ChatListFilterTabEntry]) -> Int? = { id, entries in + var index = 0 + for entry in entries { + switch entry { + case .all: + if entry.id == id { + return nil + } + case .filter: + if entry.id == id { + return index + } + index += 1 + } + } + return nil + } + let selectedTab: HorizontalTabsComponent.Tab.Id switch self.effectiveContainerNode.currentItemFilter { case .all: @@ -1553,10 +1571,9 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { var isDisabled = false if let filtersLimit = tabContainerData.2 { - guard let folderIndex = tabContainerData.0.firstIndex(where: { $0.id == mappedId }) else { - return + if let folderIndex = folderFilterIndex(mappedId, tabContainerData.0) { + isDisabled = !isPremium && folderIndex >= filtersLimit } - isDisabled = !isPremium && folderIndex >= filtersLimit } if isDisabled { @@ -1599,10 +1616,9 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate { var isDisabled = false if let filtersLimit = tabContainerData.2 { - guard let folderIndex = tabContainerData.0.firstIndex(where: { $0.id == entry.id }) else { - return + if let folderIndex = folderFilterIndex(entry.id, tabContainerData.0) { + isDisabled = !isPremium && folderIndex >= filtersLimit } - isDisabled = !isPremium && folderIndex >= filtersLimit } self.controller?.tabContextGesture(id: mappedId, sourceNode: nil, sourceView: sourceView, gesture: gesture, keepInPlace: false, isDisabled: isDisabled) diff --git a/submodules/Emoji/Sources/EmojiUtils.swift b/submodules/Emoji/Sources/EmojiUtils.swift index 4b5033e7b8..b31549fcd6 100644 --- a/submodules/Emoji/Sources/EmojiUtils.swift +++ b/submodules/Emoji/Sources/EmojiUtils.swift @@ -57,6 +57,18 @@ public extension UnicodeScalar { private final class FrameworkClass: NSObject { } +private let allowedEmojiLikeSymbols: Set = [ + "\u{2640}", + "\u{2640}\u{FE0E}", + "\u{2640}\u{FE0F}", + "\u{2642}", + "\u{2642}\u{FE0E}", + "\u{2642}\u{FE0F}", + "\u{26A7}", + "\u{26A7}\u{FE0E}", + "\u{26A7}\u{FE0F}" +] + public extension String { func trimmingTrailingSpaces() -> String { var t = self @@ -74,6 +86,20 @@ public extension String { return self.contains { $0.isEmoji } } + var containsGraphicEmoji: Bool { + var containsEmoji = false + self.enumerateSubstrings(in: self.startIndex ..< self.endIndex, options: .byComposedCharacterSequences) { substring, _, _, stop in + guard let substring else { + return + } + if substring.containsEmoji && !allowedEmojiLikeSymbols.contains(substring) { + containsEmoji = true + stop = true + } + } + return containsEmoji + } + var containsOnlyEmoji: Bool { return !self.isEmpty && !self.contains { !$0.isEmoji } } diff --git a/submodules/InstantPageUI/BUILD b/submodules/InstantPageUI/BUILD index 52f8e2ec5f..34ee3765ae 100644 --- a/submodules/InstantPageUI/BUILD +++ b/submodules/InstantPageUI/BUILD @@ -17,6 +17,7 @@ swift_library( "//submodules/TelegramCore:TelegramCore", "//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/TelegramUIPreferences:TelegramUIPreferences", + "//submodules/TextFormat:TextFormat", "//submodules/GalleryUI:GalleryUI", "//submodules/MusicAlbumArtResources:MusicAlbumArtResources", "//submodules/LiveLocationPositionNode:LiveLocationPositionNode", diff --git a/submodules/InstantPageUI/Sources/InstantPageLayout.swift b/submodules/InstantPageUI/Sources/InstantPageLayout.swift index a19a069f7f..2f07bcd4dc 100644 --- a/submodules/InstantPageUI/Sources/InstantPageLayout.swift +++ b/submodules/InstantPageUI/Sources/InstantPageLayout.swift @@ -6,6 +6,7 @@ import TelegramPresentationData import TelegramUIPreferences import TelegramStringFormatting import MosaicLayout +import TextFormat public final class InstantPageLayout { public let origin: CGPoint @@ -28,8 +29,7 @@ public final class InstantPageLayout { } } -private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantPageTheme, category: InstantPageTextCategoryType, link: Bool) { - let attributes = theme.textCategories.attributes(type: category, link: link) +private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantPageTheme, attributes: InstantPageTextAttributes) { stack.push(.textColor(attributes.color)) stack.push(.markerColor(theme.markerColor)) stack.push(.linkColor(theme.linkColor)) @@ -47,7 +47,91 @@ private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantP } } -public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: MediaResourceUserLocation, rtl: Bool, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, safeInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToSize: CGSize?, media: [EngineMedia.Id: EngineMedia], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, detailsIndexCounter: inout Int, theme: InstantPageTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], excludeCaptions: Bool) -> InstantPageLayout { +private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantPageTheme, category: InstantPageTextCategoryType, link: Bool) { + setupStyleStack(stack, theme: theme, attributes: theme.textCategories.attributes(type: category, link: link)) +} + +private func instantPageFont(style: InstantPageTextAttributes, bold: Bool = false, italic: Bool = false, fixed: Bool = false) -> UIFont { + let size = style.font.size + if fixed { + if bold && italic { + return UIFont(name: "Menlo-BoldItalic", size: size) ?? Font.semiboldItalic(size) + } else if bold { + return UIFont(name: "Menlo-Bold", size: size) ?? Font.bold(size) + } else if italic { + return UIFont(name: "Menlo-Italic", size: size) ?? Font.italic(size) + } else { + return UIFont(name: "Menlo", size: size) ?? Font.regular(size) + } + } + switch style.font.style { + case .serif: + if bold && italic { + return UIFont(name: "Georgia-BoldItalic", size: size) ?? Font.semiboldItalic(size) + } else if bold { + return UIFont(name: "Georgia-Bold", size: size) ?? Font.bold(size) + } else if italic { + return UIFont(name: "Georgia-Italic", size: size) ?? Font.italic(size) + } else { + return UIFont(name: "Georgia", size: size) ?? Font.regular(size) + } + case .sans: + if bold && italic { + return Font.semiboldItalic(size) + } else if bold { + return Font.bold(size) + } else if italic { + return Font.italic(size) + } else { + return Font.regular(size) + } + } +} + +private func attributedStringForPreformattedText(_ text: RichText, language: String?, theme: InstantPageTheme, cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight?) -> NSAttributedString { + let paragraphAttributes = theme.textCategories.attributes(type: .paragraph, link: false) + let textValue = text.plainText + guard !textValue.isEmpty else { + return NSAttributedString( + string: "", + attributes: [ + .font: instantPageFont(style: paragraphAttributes, fixed: true), + .foregroundColor: paragraphAttributes.color, + NSAttributedString.Key(rawValue: InstantPageLineSpacingFactorAttribute): paragraphAttributes.font.lineSpacingFactor as NSNumber + ] + ) + } + + let attributedString = stringWithAppliedEntities( + textValue, + entities: [ + MessageTextEntity(range: 0 ..< (textValue as NSString).length, type: .Pre(language: language)) + ], + baseColor: paragraphAttributes.color, + linkColor: theme.linkColor, + codeBlockTitleColor: paragraphAttributes.color, + codeBlockAccentColor: paragraphAttributes.color, + codeBlockBackgroundColor: theme.codeBlockBackgroundColor, + baseFont: instantPageFont(style: paragraphAttributes), + linkFont: instantPageFont(style: paragraphAttributes), + boldFont: instantPageFont(style: paragraphAttributes, bold: true), + italicFont: instantPageFont(style: paragraphAttributes, italic: true), + boldItalicFont: instantPageFont(style: paragraphAttributes, bold: true, italic: true), + fixedFont: instantPageFont(style: paragraphAttributes, fixed: true), + blockQuoteFont: instantPageFont(style: paragraphAttributes), + underlineLinks: false, + message: nil, + cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight + ).mutableCopy() as! NSMutableAttributedString + attributedString.addAttribute( + NSAttributedString.Key(rawValue: InstantPageLineSpacingFactorAttribute), + value: paragraphAttributes.font.lineSpacingFactor as NSNumber, + range: NSRange(location: 0, length: attributedString.length) + ) + return attributedString +} + +public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: MediaResourceUserLocation, rtl: Bool, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, safeInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToSize: CGSize?, media: [EngineMedia.Id: EngineMedia], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, detailsIndexCounter: inout Int, theme: InstantPageTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil, excludeCaptions: Bool) -> InstantPageLayout { let layoutCaption: (InstantPageCaption, CGSize) -> ([InstantPageItem], CGSize) = { caption, contentSize in var items: [InstantPageItem] = [] @@ -100,7 +184,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: switch block { case let .cover(block): - return layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: true, previousItems:previousItems, fillToSize: fillToSize, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false) + return layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: true, previousItems:previousItems, fillToSize: fillToSize, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false) case let .title(text): let styleStack = InstantPageTextStyleStack() setupStyleStack(styleStack, theme: theme, category: .header, link: false) @@ -168,16 +252,34 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: setupStyleStack(styleStack, theme: theme, category: .subheader, link: false) let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage) return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) + case let .heading(text, level): + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, attributes: theme.headingTextAttributes(level: level, link: false)) + let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage) + return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) case let .paragraph(text): let styleStack = InstantPageTextStyleStack() setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, horizontalInset: horizontalInset, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage) return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items) - case let .preformatted(text): - let styleStack = InstantPageTextStyleStack() - setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) + case let .preformatted(text, language): let backgroundInset: CGFloat = 14.0 - let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - backgroundInset * 2.0, offset: CGPoint(x: 17.0, y: backgroundInset), media: media, webpage: webpage, opaqueBackground: true) + let attributedString: NSAttributedString + if let language, !language.isEmpty { + attributedString = attributedStringForPreformattedText(text, language: language, theme: theme, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight) + } else { + let styleStack = InstantPageTextStyleStack() + setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) + attributedString = attributedStringForRichText(text, styleStack: styleStack) + } + let (_, items, contentSize) = layoutTextItemWithString( + attributedString, + boundingWidth: boundingWidth - horizontalInset * 2.0 - backgroundInset * 2.0, + offset: CGPoint(x: 17.0, y: backgroundInset), + media: media, + webpage: webpage, + opaqueBackground: true + ) let backgroundItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: contentSize.height + backgroundInset * 2.0)), shapeFrame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: contentSize.height + backgroundInset * 2.0)), shape: .rect, color: theme.codeBlockBackgroundColor) var allItems: [InstantPageItem] = [backgroundItem] allItems.append(contentsOf: items) @@ -274,7 +376,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: var previousBlock: InstantPageBlock? var originY: CGFloat = contentSize.height for subBlock in blocks { - let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - indexSpacing - maxIndexWidth, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: listItems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false) + let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - indexSpacing - maxIndexWidth, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: listItems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false) let spacing: CGFloat = previousBlock != nil && subLayout.contentSize.height > 0.0 ? spacingBetweenBlocks(upper: previousBlock, lower: subBlock) : 0.0 let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: horizontalInset + indexSpacing + maxIndexWidth, y: contentSize.height + spacing)) @@ -476,7 +578,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: var i = 0 for subItem in innerItems { let frame = mosaicLayout[i].0 - let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subItem, boundingWidth: frame.width, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: frame.size, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: true) + let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subItem, boundingWidth: frame.width, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: frame.size, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: true) items.append(contentsOf: subLayout.flattenedItemsWithOrigin(frame.origin)) i += 1 } @@ -550,7 +652,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: var previousBlock: InstantPageBlock? for subBlock in blocks { - let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false) + let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false) let spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock) let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: horizontalInset + lineInset, y: contentSize.height + spacing)) @@ -733,7 +835,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: var previousBlock: InstantPageBlock? for subBlock in blocks { - let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: false, previousItems: subitems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &subDetailsIndex, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false) + let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: false, previousItems: subitems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &subDetailsIndex, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false) let spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock) let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing)) @@ -842,7 +944,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: } } -public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, instantPage: InstantPage?, userLocation: MediaResourceUserLocation, boundingWidth: CGFloat, safeInset: CGFloat, strings: PresentationStrings, theme: InstantPageTheme, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:]) -> InstantPageLayout { +public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, instantPage: InstantPage?, userLocation: MediaResourceUserLocation, boundingWidth: CGFloat, safeInset: CGFloat, strings: PresentationStrings, theme: InstantPageTheme, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil) -> InstantPageLayout { var maybeLoadedContent: TelegramMediaWebpageLoadedContent? if case let .Loaded(content) = webPage.content { maybeLoadedContent = content @@ -871,7 +973,7 @@ public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, instant var previousBlock: InstantPageBlock? for block in pageBlocks { - let blockLayout = layoutInstantPageBlock(webpage: webPage, userLocation: userLocation, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: 17.0 + safeInset, safeInset: safeInset, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false) + let blockLayout = layoutInstantPageBlock(webpage: webPage, userLocation: userLocation, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: 17.0 + safeInset, safeInset: safeInset, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false) let spacing = spacingBetweenBlocks(upper: previousBlock, lower: block) let blockItems = blockLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing)) items.append(contentsOf: blockItems) diff --git a/submodules/InstantPageUI/Sources/InstantPageLayoutSpacings.swift b/submodules/InstantPageUI/Sources/InstantPageLayoutSpacings.swift index 6ad2c61e7c..51cc7d4886 100644 --- a/submodules/InstantPageUI/Sources/InstantPageLayoutSpacings.swift +++ b/submodules/InstantPageUI/Sources/InstantPageLayoutSpacings.swift @@ -21,7 +21,7 @@ func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?) -> return 20.0 case (.title, .paragraph), (.authorDate, .paragraph): return 34.0 - case (.header, .paragraph), (.subheader, .paragraph): + case (.header, .paragraph), (.subheader, .paragraph), (.heading, .paragraph): return 25.0 case (.list, .paragraph): return 31.0 @@ -33,7 +33,7 @@ func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?) -> return 20.0 case (.title, .list), (.authorDate, .list): return 34.0 - case (.header, .list), (.subheader, .list): + case (.header, .list), (.subheader, .list), (.heading, .list): return 31.0 case (.preformatted, .list): return 19.0 @@ -43,7 +43,7 @@ func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?) -> return 19.0 case (_, .preformatted): return 20.0 - case (_, .header), (_, .subheader): + case (_, .header), (_, .subheader), (_, .heading): return 32.0 default: return 20.0 diff --git a/submodules/InstantPageUI/Sources/InstantPageTheme.swift b/submodules/InstantPageUI/Sources/InstantPageTheme.swift index 0c1b1d8153..dccc23d9fd 100644 --- a/submodules/InstantPageUI/Sources/InstantPageTheme.swift +++ b/submodules/InstantPageUI/Sources/InstantPageTheme.swift @@ -135,6 +135,32 @@ public final class InstantPageTheme { public func withUpdatedFontStyles(sizeMultiplier: CGFloat, forceSerif: Bool) -> InstantPageTheme { return InstantPageTheme(type: type, pageBackgroundColor: pageBackgroundColor, textCategories: self.textCategories.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), serif: forceSerif, codeBlockBackgroundColor: codeBlockBackgroundColor, linkColor: linkColor, textHighlightColor: textHighlightColor, linkHighlightColor: linkHighlightColor, markerColor: markerColor, panelBackgroundColor: panelBackgroundColor, panelHighlightedBackgroundColor: panelHighlightedBackgroundColor, panelPrimaryColor: panelPrimaryColor, panelSecondaryColor: panelSecondaryColor, panelAccentColor: panelAccentColor, tableBorderColor: tableBorderColor, tableHeaderColor: tableHeaderColor, controlColor: controlColor, imageTintColor: imageTintColor, overlayPanelColor: overlayPanelColor) } + + func headingTextAttributes(level: Int32, link: Bool) -> InstantPageTextAttributes { + let clampedLevel = max(Int32(3), min(level, Int32(6))) + let subheaderAttributes = self.textCategories.subheader + guard clampedLevel > 3 else { + return subheaderAttributes.withUnderline(link) + } + + let baseSize: CGFloat + switch clampedLevel { + case 4: + baseSize = 17.0 + case 5: + baseSize = 15.0 + default: + baseSize = 13.0 + } + + let sizeMultiplier = subheaderAttributes.font.size / 19.0 + let attributes = InstantPageTextAttributes( + font: InstantPageFont(style: .serif, size: floor(baseSize * sizeMultiplier), lineSpacingFactor: subheaderAttributes.font.lineSpacingFactor), + color: subheaderAttributes.color, + underline: subheaderAttributes.underline + ) + return attributes.withUnderline(link) + } } private let lightTheme = InstantPageTheme( diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCarouselItemView.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCarouselItemView.h index 38d1fdfa9f..3ce8d62e49 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCarouselItemView.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGAttachmentCarouselItemView.h @@ -35,7 +35,7 @@ @property (nonatomic) bool reminder; @property (nonatomic) bool forum; @property (nonatomic) bool isSuggesting; -@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t)); +@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t, bool)); @property (nonatomic, copy) void (^presentTimerController)(void (^)(int32_t)); @property (nonatomic, strong) NSArray *underlyingViews; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraController.h index d1e0b2fcc0..7e1164ee05 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGCameraController.h @@ -63,7 +63,7 @@ typedef enum { @property (nonatomic, copy) void(^finishedTransitionOut)(void); @property (nonatomic, copy) void(^customPresentOverlayController)(TGOverlayController *(^)(id)); -@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t)); +@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t, bool)); @property (nonatomic, copy) void (^presentTimerController)(void (^)(int32_t)); - (instancetype)initWithContext:(id)context saveEditedPhotos:(bool)saveEditedPhotos saveCapturedMedia:(bool)saveCapturedMedia; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAssetsController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAssetsController.h index 178f043270..1578e0f772 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAssetsController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaAssetsController.h @@ -70,7 +70,7 @@ typedef enum @property (nonatomic, assign) bool forum; @property (nonatomic, assign) bool isSuggesting; -@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t)); +@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t, bool)); @property (nonatomic, copy) void (^presentTimerController)(void (^)(int32_t)); @property (nonatomic, assign) bool liveVideoUploadEnabled; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerController.h index c256f0832c..42ade02f88 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerController.h @@ -35,7 +35,7 @@ @property (nonatomic, assign) bool forum; @property (nonatomic, assign) bool isSuggesting; -@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t)); +@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t, bool)); @property (nonatomic, copy) void (^presentTimerController)(void (^)(int32_t)); @property (nonatomic, assign) CGFloat topInset; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerModernGalleryMixin.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerModernGalleryMixin.h index 24f4876030..46aacd32a1 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerModernGalleryMixin.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGMediaPickerModernGalleryMixin.h @@ -28,7 +28,7 @@ @property (nonatomic, copy) void (^editorOpened)(void); @property (nonatomic, copy) void (^editorClosed)(void); -@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t)); +@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t, bool)); @property (nonatomic, copy) void (^presentTimerController)(void (^)(int32_t)); - (instancetype)initWithContext:(id)context item:(id)item fetchResult:(TGMediaAssetFetchResult *)fetchResult parentController:(TGViewController *)parentController thumbnailImage:(UIImage *)thumbnailImage selectionContext:(TGMediaSelectionContext *)selectionContext editingContext:(TGMediaEditingContext *)editingContext hasCaptions:(bool)hasCaptions allowCaptionEntities:(bool)allowCaptionEntities hasTimer:(bool)hasTimer onlyCrop:(bool)onlyCrop inhibitDocumentCaptions:(bool)inhibitDocumentCaptions inhibitMute:(bool)inhibitMute asFile:(bool)asFile itemsLimit:(NSUInteger)itemsLimit recipientName:(NSString *)recipientName hasSilentPosting:(bool)hasSilentPosting hasSchedule:(bool)hasSchedule reminder:(bool)reminder hasCoverButton:(bool)hasCoverButton stickersContext:(id)stickersContext; diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoVideoEditor.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoVideoEditor.h index 97fa637d8d..2031d9610e 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoVideoEditor.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGPhotoVideoEditor.h @@ -9,7 +9,7 @@ @class TGModernGalleryController; -typedef void (^ _Nonnull TGPhotoVideoEditorSchedulePickerCompletion)(int32_t time); +typedef void (^ _Nonnull TGPhotoVideoEditorSchedulePickerCompletion)(int32_t time, bool silentPosting); typedef void (^ _Nonnull TGPhotoVideoEditorSchedulePicker)(bool media, TGPhotoVideoEditorSchedulePickerCompletion _Nonnull done); typedef void (^ _Nonnull TGPhotoVideoEditorCompletion)(id _Nonnull item, TGMediaEditingContext * _Nonnull editingContext, bool silentPosting, int32_t scheduleTime); diff --git a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoMessageCaptureController.h b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoMessageCaptureController.h index 1538db8304..99af708e39 100644 --- a/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoMessageCaptureController.h +++ b/submodules/LegacyComponents/PublicHeaders/LegacyComponents/TGVideoMessageCaptureController.h @@ -31,7 +31,7 @@ @property (nonatomic, copy) void(^didDismiss)(void); @property (nonatomic, copy) void(^didStop)(void); @property (nonatomic, copy) void(^displaySlowmodeTooltip)(void); -@property (nonatomic, copy) void (^presentScheduleController)(void (^)(int32_t)); +@property (nonatomic, copy) void (^presentScheduleController)(void (^)(int32_t, bool)); - (instancetype)initWithContext:(id)context forStory:(bool)forStory assets:(TGVideoMessageCaptureControllerAssets *)assets transitionInView:(UIView *(^)(void))transitionInView parentController:(TGViewController *)parentController controlsFrame:(CGRect)controlsFrame isAlreadyLocked:(bool (^)(void))isAlreadyLocked liveUploadInterface:(id)liveUploadInterface pallete:(TGModernConversationInputMicPallete *)pallete slowmodeTimestamp:(int32_t)slowmodeTimestamp slowmodeView:(UIView *(^)(void))slowmodeView canSendSilently:(bool)canSendSilently canSchedule:(bool)canSchedule reminder:(bool)reminder; diff --git a/submodules/LegacyComponents/Sources/TGCameraController.m b/submodules/LegacyComponents/Sources/TGCameraController.m index fd9b44075e..3d9f789212 100644 --- a/submodules/LegacyComponents/Sources/TGCameraController.m +++ b/submodules/LegacyComponents/Sources/TGCameraController.m @@ -1656,7 +1656,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus if (strongSelf == nil) return; - strongSelf.presentScheduleController(true, ^(int32_t time) { + strongSelf.presentScheduleController(true, ^(int32_t time, bool silentPosting) { __strong TGCameraController *strongSelf = weakSelf; __strong TGMediaPickerGalleryModel *strongModel = weakModel; @@ -1678,7 +1678,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus [[NSUserDefaults standardUserDefaults] setObject:@(!strongSelf->_selectionContext.grouping) forKey:@"TG_mediaGroupingDisabled_v0"]; if (strongSelf.finishedWithResults != nil) - strongSelf.finishedWithResults(strongController, strongSelf->_selectionContext, strongSelf->_editingContext, item.asset, false, time); + strongSelf.finishedWithResults(strongController, strongSelf->_selectionContext, strongSelf->_editingContext, item.asset, silentPosting, time); [strongSelf _dismissTransitionForResultController:strongController]; }); diff --git a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m index 1522c48030..4ff21ec6d9 100644 --- a/submodules/LegacyComponents/Sources/TGMediaAssetsController.m +++ b/submodules/LegacyComponents/Sources/TGMediaAssetsController.m @@ -478,7 +478,7 @@ static TGVideoEditAdjustments *TGMediaAssetsPatchedLivePhotoAdjustments(PGPhotoE self.pickerController.isSuggesting = isSuggesting; } -- (void)setPresentScheduleController:(void (^)(bool, void (^)(int32_t)))presentScheduleController { +- (void)setPresentScheduleController:(void (^)(bool, void (^)(int32_t, bool)))presentScheduleController { _presentScheduleController = [presentScheduleController copy]; self.pickerController.presentScheduleController = presentScheduleController; } @@ -1890,8 +1890,8 @@ static TGVideoEditAdjustments *TGMediaAssetsPatchedLivePhotoAdjustments(PGPhotoE - (void)schedule:(bool)media { __weak TGMediaAssetsController *weakSelf = self; - self.presentScheduleController(media, ^(int32_t scheduleTime) { - [weakSelf completeWithCurrentItem:nil silentPosting:false scheduleTime:scheduleTime]; + self.presentScheduleController(media, ^(int32_t scheduleTime, bool silentPosting) { + [weakSelf completeWithCurrentItem:nil silentPosting:silentPosting scheduleTime:scheduleTime]; }); } diff --git a/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m b/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m index a1f83e5124..35a79b3278 100644 --- a/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m +++ b/submodules/LegacyComponents/Sources/TGMediaPickerModernGalleryMixin.m @@ -201,7 +201,7 @@ if (strongSelf == nil) return; - strongSelf.presentScheduleController(true, ^(int32_t time) { + strongSelf.presentScheduleController(true, ^(int32_t time, bool silentPosting) { __strong TGMediaPickerModernGalleryMixin *strongSelf = weakSelf; if (strongSelf == nil) return; @@ -209,7 +209,7 @@ strongSelf->_galleryModel.dismiss(true, false); if (strongSelf.completeWithItem != nil) - strongSelf.completeWithItem(item, false, time); + strongSelf.completeWithItem(item, silentPosting, time); }); }; controller.sendWithTimer = ^{ diff --git a/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m b/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m index 5ebd25410c..c407675ed9 100644 --- a/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m +++ b/submodules/LegacyComponents/Sources/TGPhotoVideoEditor.m @@ -282,8 +282,8 @@ complete(false, 0x7ffffffe); }; sendController.schedule = ^{ - presentSchedulePicker(true, ^(int32_t time) { - complete(false, time); + presentSchedulePicker(true, ^(int32_t time, bool silentPosting) { + complete(silentPosting, time); }); }; [strongController presentViewController:sendController animated:false completion:nil]; diff --git a/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m b/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m index 3a410ff2d7..cba0bf530e 100644 --- a/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m +++ b/submodules/LegacyComponents/Sources/TGVideoMessageCaptureController.m @@ -844,13 +844,13 @@ typedef enum } if (strongSelf.presentScheduleController) { - strongSelf.presentScheduleController(^(int32_t time) { + strongSelf.presentScheduleController(^(int32_t time, bool silentPosting) { __strong TGVideoMessageCaptureController *strongSelf = weakSelf; if (strongSelf == nil) { return; } - [strongSelf finishWithURL:strongSelf->_url dimensions:CGSizeMake(240.0f, 240.0f) duration:strongSelf->_duration liveUploadData:strongSelf->_liveUploadData thumbnailImage:strongSelf->_thumbnailImage isSilent:false scheduleTimestamp:time]; + [strongSelf finishWithURL:strongSelf->_url dimensions:CGSizeMake(240.0f, 240.0f) duration:strongSelf->_duration liveUploadData:strongSelf->_liveUploadData thumbnailImage:strongSelf->_thumbnailImage isSilent:silentPosting scheduleTimestamp:time]; _automaticDismiss = true; [strongSelf dismiss:false]; diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift index 0fd7d992a4..d367a967c3 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyAttachmentMenu.swift @@ -170,7 +170,7 @@ public func legacyMediaEditor( hasSilentPosting: Bool = false, hasSchedule: Bool = false, reminder: Bool = false, - presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void = { _, _ in }, + presentSchedulePicker: @escaping (Bool, @escaping (Int32, Bool) -> Void) -> Void = { _, _ in }, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, Bool) -> Void, present: @escaping (ViewController, Any?) -> Void ) { @@ -213,7 +213,7 @@ public func legacyMediaEditor( legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true } - let schedulePicker: (Bool, @escaping (Int32) -> Void) -> Void = { media, done in + let schedulePicker: (Bool, @escaping (Int32, Bool) -> Void) -> Void = { media, done in presentSchedulePicker(media, done) } let appeared: () -> Void = { @@ -322,7 +322,7 @@ public func legacyAttachmentMenu( presentSelectionLimitExceeded: @escaping () -> Void, presentCantSendMultipleFiles: @escaping () -> Void, presentJpegConversionAlert: @escaping (@escaping (Bool) -> Void) -> Void, - presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, + presentSchedulePicker: @escaping (Bool, @escaping (Int32, Bool) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ((String) -> UIView?)?, @escaping () -> Void) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void, @@ -431,8 +431,8 @@ public func legacyAttachmentMenu( carouselItem.hasSchedule = hasSchedule carouselItem.reminder = peer?.id == context.account.peerId carouselItem.presentScheduleController = { media, done in - presentSchedulePicker(media, { time in - done?(time) + presentSchedulePicker(media, { time, silentPosting in + done?(time, silentPosting) }) } carouselItem.presentTimerController = { done in diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift index 17ab15a722..a4ebc8517e 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyMediaPickers.swift @@ -20,7 +20,7 @@ public func guessMimeTypeByFileExtension(_ ext: String) -> String { return TGMimeTypeMap.mimeType(forExtension: ext) ?? "application/binary" } -public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, context: AccountContext, peer: Peer, chatLocation: ChatLocation, captionsEnabled: Bool = true, storeCreatedAssets: Bool = true, showFileTooltip: Bool = false, initialCaption: NSAttributedString, hasSchedule: Bool, presentWebSearch: (() -> Void)?, presentSelectionLimitExceeded: @escaping () -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?) { +public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, context: AccountContext, peer: Peer, chatLocation: ChatLocation, captionsEnabled: Bool = true, storeCreatedAssets: Bool = true, showFileTooltip: Bool = false, initialCaption: NSAttributedString, hasSchedule: Bool, presentWebSearch: (() -> Void)?, presentSelectionLimitExceeded: @escaping () -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32, Bool) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?) { let paintStickersContext = LegacyPaintStickersContext(context: context) paintStickersContext.captionPanelView = { return getCaptionPanelView() @@ -43,8 +43,8 @@ public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, co controller.hasCoverButton = true } controller.presentScheduleController = { media, done in - presentSchedulePicker(media, { time in - done?(time) + presentSchedulePicker(media, { time, silentPosting in + done?(time, silentPosting) }) } controller.presentTimerController = { done in diff --git a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift index 01ecb788f3..a2da740107 100644 --- a/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift +++ b/submodules/MediaPickerUI/Sources/LegacyMediaPickerGallery.swift @@ -131,7 +131,7 @@ func presentLegacyMediaPickerGallery( transitionHostView: @escaping () -> UIView?, transitionView: @escaping (String) -> UIView?, completed: @escaping (TGMediaSelectableItem & TGMediaEditableItem, Bool, Int32?, @escaping () -> Void) -> Void, - presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, + presentSchedulePicker: @escaping (Bool, @escaping (Int32, Bool) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: @escaping (ViewController, Any?) -> Void, @@ -369,8 +369,8 @@ func presentLegacyMediaPickerGallery( }) } sheetController.schedule = { - presentSchedulePicker(true, { time in - completed(item.asset, false, time, { + presentSchedulePicker(true, { time, silentPosting in + completed(item.asset, silentPosting, time, { dismissImpl() }) }) diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 62e456fc28..8e176c8f43 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -248,7 +248,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att public weak var webSearchController: WebSearchController? public var openCamera: ((Any?) -> Void)? - public var presentSchedulePicker: (Bool, @escaping (Int32) -> Void) -> Void = { _, _ in } + public var presentSchedulePicker: (Bool, @escaping (Int32, Bool) -> Void) -> Void = { _, _ in } public var presentTimerPicker: (@escaping (Int32) -> Void) -> Void = { _ in } public var presentWebSearch: (MediaGroupsScreen, Bool) -> Void = { _, _ in } public var getCaptionPanelView: () -> TGCaptionPanelView? = { return nil } @@ -2298,8 +2298,8 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att } }, schedule: { [weak self] parameters in if let strongSelf = self { - strongSelf.presentSchedulePicker(false, { [weak self] time in - self?.interaction?.sendSelected(nil, false, time, true, parameters, {}) + strongSelf.presentSchedulePicker(false, { [weak self] time, silentPosting in + self?.interaction?.sendSelected(nil, silentPosting, time, true, parameters, {}) }) } }, dismissInput: { [weak self] in diff --git a/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift b/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift index a744c229dd..680b6eb9ee 100644 --- a/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift +++ b/submodules/NotificationSoundSelectionUI/Sources/NotificationSoundSelection.swift @@ -476,7 +476,7 @@ public func notificationSoundSelectionController(context: AccountContext, update break } }), - TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: { + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { }) ], parseMarkdown: true), in: .window(.root)) } diff --git a/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationControllerNode.swift b/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationControllerNode.swift index 692d1e07cf..aa64693954 100644 --- a/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationControllerNode.swift +++ b/submodules/PasswordSetupUI/Sources/SetupTwoStepVerificationControllerNode.swift @@ -697,7 +697,7 @@ final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode { }, transition: .animated(duration: 0.5, curve: .spring)) } if case let .enterEmail(enterEmailState, enterEmailEmail)? = self.innerState.data.state, case .create = enterEmailState, enterEmailEmail.isEmpty { - self.present(textAlertController(sharedContext: self.context.sharedContext, title: nil, text: self.presentationData.strings.TwoStepAuth_EmailSkipAlert, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: self.presentationData.strings.TwoStepAuth_EmailSkip, action: { + self.present(textAlertController(sharedContext: self.context.sharedContext, title: nil, text: self.presentationData.strings.TwoStepAuth_EmailSkipAlert, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: self.presentationData.strings.TwoStepAuth_EmailSkip, action: { continueImpl() })]), nil) } else { diff --git a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift index 78bd4bfa78..cf77e35974 100644 --- a/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift +++ b/submodules/PasswordSetupUI/Sources/TwoFactorAuthDataInputScreen.swift @@ -534,7 +534,7 @@ public final class TwoFactorDataInputScreen: ViewController { strongSelf.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: alertText, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) }) }), - TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}) + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}) ]), in: .window(.root)) case let .passwordHint(recovery, password, doneText): if let recovery = recovery { @@ -550,7 +550,7 @@ public final class TwoFactorDataInputScreen: ViewController { } strongSelf.performRecovery(recovery: recovery, password: "", hint: "") }), - TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}) + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}) ]), in: .window(.root)) case let .rememberPassword(doneText): guard case let .authorized(engine) = strongSelf.engine else { diff --git a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift index e433428491..32661edbe5 100644 --- a/submodules/PeerInfoUI/Sources/ChannelAdminController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelAdminController.swift @@ -385,7 +385,7 @@ private enum ChannelAdminEntry: ItemListNodeEntry { return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(string: "", textColor: .black), text: text, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: true), spacing: 0.0, clearType: enabled ? .always : .none, enabled: enabled, tag: ChannelAdminEntryTag.rank, sectionId: self.section, textUpdated: { updatedText in arguments.updateRank(text, updatedText) }, shouldUpdateText: { text in - if text.containsEmoji { + if text.containsGraphicEmoji { arguments.animateError() return false } @@ -1329,7 +1329,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD return current } - if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsEmoji { + if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsGraphicEmoji { errorImpl?() return } @@ -1361,7 +1361,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD } let effectiveRank = updateRank ?? currentRank - if effectiveRank?.containsEmoji ?? false { + if effectiveRank?.containsGraphicEmoji ?? false { errorImpl?() return } @@ -1436,7 +1436,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD return current } - if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsEmoji { + if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsGraphicEmoji { errorImpl?() return } @@ -1516,7 +1516,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD return current } - if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsEmoji { + if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsGraphicEmoji { errorImpl?() return } diff --git a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift index 2c0fe628f0..bbf69ed1ef 100644 --- a/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelBannedMemberController.swift @@ -382,7 +382,7 @@ private enum ChannelBannedMemberEntry: ItemListNodeEntry { return ItemListSingleLineInputItem(presentationData: presentationData, systemStyle: .glass, title: NSAttributedString(string: "", textColor: .black), text: text, placeholder: placeholder, type: .regular(capitalization: false, autocorrection: true), spacing: 0.0, clearType: enabled ? .always : .none, enabled: enabled, tag: ChannelBannedMemberEntryTag.rank, sectionId: self.section, textUpdated: { updatedText in arguments.updateRank(text, updatedText) }, shouldUpdateText: { text in - if text.containsEmoji { + if text.containsGraphicEmoji { arguments.animateError() return false } @@ -860,7 +860,7 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen updateRank = current.updatedRank?.trimmingCharacters(in: .whitespacesAndNewlines) return current } - if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsEmoji { + if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsGraphicEmoji { errorImpl?() return } diff --git a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift index d03290333d..1ccf724f2f 100644 --- a/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift +++ b/submodules/PeerInfoUI/Sources/ChannelPermissionsController.swift @@ -534,6 +534,8 @@ func stringForGroupPermission(strings: PresentationStrings, right: TelegramChatB return strings.Channel_BanUser_PermissionSendVoiceMessage } else if right.contains(.banSendInstantVideos) { return strings.Channel_BanUser_PermissionSendVideoMessage + } else if right.contains(.banSendReactions) { + return strings.Channel_BanUser_PermissionSendReactions } else if right.contains(.banEditRank) { if defaultPermissions { return strings.Channel_BanUser_PermissionEditOwnRank @@ -568,6 +570,8 @@ func compactStringForGroupPermission(strings: PresentationStrings, right: Telegr return strings.GroupPermission_NoSendLinks } else if right.contains(.banSendPolls) { return strings.GroupPermission_NoSendPolls + } else if right.contains(.banSendReactions) { + return strings.GroupPermission_NoSendReactions } else if right.contains(.banChangeInfo) { return strings.GroupPermission_NoChangeInfo } else if right.contains(.banAddMembers) { @@ -595,6 +599,7 @@ private let internal_allPossibleGroupPermissionList: [(TelegramChatBannedRightsF (.banSendInstantVideos, .banMembers), (.banEmbedLinks, .banMembers), (.banSendPolls, .banMembers), + (.banSendReactions, .banMembers), (.banAddMembers, .banMembers), (.banPinMessages, .pinMessages), (.banManageTopics, .manageTopics), @@ -647,6 +652,7 @@ public func banSendMediaSubList() -> [(TelegramChatBannedRightsFlags, TelegramCh (.banSendInstantVideos, .banMembers), (.banEmbedLinks, .banMembers), (.banSendPolls, .banMembers), + (.banSendReactions, .banMembers) ] } diff --git a/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift b/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift index d0b5427be0..e96125f1e7 100644 --- a/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift +++ b/submodules/SettingsUI/Sources/Privacy and Security/CreatePasswordController.swift @@ -313,7 +313,7 @@ func createPasswordController(context: AccountContext, createPasswordContext: Cr } if emailAlert { - presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.TwoStepAuth_EmailSkipAlert, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: presentationData.strings.TwoStepAuth_EmailSkip, action: { + presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.TwoStepAuth_EmailSkipAlert, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: presentationData.strings.TwoStepAuth_EmailSkip, action: { saveImpl() })]), nil) } else { diff --git a/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceController.swift b/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceController.swift index 2317be18fb..b85aaeea8c 100644 --- a/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceController.swift +++ b/submodules/SettingsUI/Sources/Terms of Service/TermsOfServiceController.swift @@ -118,7 +118,7 @@ public class TermsOfServiceController: ViewController, StandalonePresentableCont } strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.PrivacyPolicy_Decline, text: text, actions: [TextAlertAction(type: .destructiveAction, title: declineTitle, action: { self?.decline() - }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { })], actionLayout: .vertical), in: .window(.root)) }, rightAction: { [weak self] in guard let strongSelf = self else { diff --git a/submodules/TelegramApi/Sources/Api40.swift b/submodules/TelegramApi/Sources/Api40.swift index caec6d9e63..6960f0e58b 100644 --- a/submodules/TelegramApi/Sources/Api40.swift +++ b/submodules/TelegramApi/Sources/Api40.swift @@ -5851,6 +5851,39 @@ public extension Api.functions.messages { }) } } +public extension Api.functions.messages { + static func deleteParticipantReaction(peer: Api.InputPeer, msgId: Int32, participant: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-474482644) + peer.serialize(buffer, true) + serializeInt32(msgId, buffer: buffer, boxed: false) + participant.serialize(buffer, true) + return (FunctionDescription(name: "messages.deleteParticipantReaction", parameters: [("peer", ConstructorParameterDescription(peer)), ("msgId", ConstructorParameterDescription(msgId)), ("participant", ConstructorParameterDescription(participant))]), buffer, DeserializeFunctionResponse { (buffer: Buffer) -> Api.Updates? in + let reader = BufferReader(buffer) + var result: Api.Updates? + if let signature = reader.readInt32() { + result = Api.parse(reader, signature: signature) as? Api.Updates + } + return result + }) + } +} +public extension Api.functions.messages { + static func deleteParticipantReactions(peer: Api.InputPeer, participant: Api.InputPeer) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { + let buffer = Buffer() + buffer.appendInt32(-1598550792) + peer.serialize(buffer, true) + participant.serialize(buffer, true) + return (FunctionDescription(name: "messages.deleteParticipantReactions", parameters: [("peer", ConstructorParameterDescription(peer)), ("participant", ConstructorParameterDescription(participant))]), 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.messages { static func deletePhoneCallHistory(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse) { let buffer = Buffer() diff --git a/submodules/TelegramCore/FlatSerialization/Models/InstantPageBlock.fbs b/submodules/TelegramCore/FlatSerialization/Models/InstantPageBlock.fbs index 5822db8b5f..ec3372afa1 100644 --- a/submodules/TelegramCore/FlatSerialization/Models/InstantPageBlock.fbs +++ b/submodules/TelegramCore/FlatSerialization/Models/InstantPageBlock.fbs @@ -34,7 +34,8 @@ union InstantPageBlock_Value { InstantPageBlock_Table, InstantPageBlock_Details, InstantPageBlock_RelatedArticles, - InstantPageBlock_Map + InstantPageBlock_Map, + InstantPageBlock_Heading } table InstantPageBlock { @@ -70,6 +71,7 @@ table InstantPageBlock_Paragraph { table InstantPageBlock_Preformatted { text:RichText (id: 0, required); + language:string (id: 1); } table InstantPageBlock_Footer { @@ -184,6 +186,11 @@ table InstantPageBlock_Map { caption:InstantPageCaption (id: 4, required); } +table InstantPageBlock_Heading { + text:RichText (id: 0, required); + level:int32 (id: 1); +} + table InstantPageCaption { text:RichText (id: 0, required); credit:RichText (id: 1, required); diff --git a/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift b/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift index 262901d2a0..7030b2176a 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/InstantPage.swift @@ -121,7 +121,7 @@ extension InstantPageBlock { self = .paragraph(RichText(apiText: text)) case let .pageBlockPreformatted(pageBlockPreformattedData): let text = pageBlockPreformattedData.text - self = .preformatted(RichText(apiText: text)) + self = .preformatted(text: RichText(apiText: text), language: nil) case let .pageBlockFooter(pageBlockFooterData): let text = pageBlockFooterData.text self = .footer(RichText(apiText: text)) diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift index a721277bd6..83072ffdf0 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_InstantPage.swift @@ -32,6 +32,7 @@ private enum InstantPageBlockType: Int32 { case details = 25 case relatedArticles = 26 case map = 27 + case heading = 28 } private func decodeListItems(_ decoder: PostboxDecoder) -> [InstantPageListItem] { @@ -60,8 +61,9 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable { case authorDate(author: RichText, date: Int32) case header(RichText) case subheader(RichText) + case heading(text: RichText, level: Int32) case paragraph(RichText) - case preformatted(RichText) + case preformatted(text: RichText, language: String?) case footer(RichText) case divider case anchor(String) @@ -97,10 +99,15 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable { self = .header(decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText) case InstantPageBlockType.subheader.rawValue: self = .subheader(decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText) + case InstantPageBlockType.heading.rawValue: + self = .heading(text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText, level: decoder.decodeInt32ForKey("l", orElse: 3)) case InstantPageBlockType.paragraph.rawValue: self = .paragraph(decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText) case InstantPageBlockType.preformatted.rawValue: - self = .preformatted(decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText) + self = .preformatted( + text: decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText, + language: decoder.decodeOptionalStringForKey("l") + ) case InstantPageBlockType.footer.rawValue: self = .footer(decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText) case InstantPageBlockType.divider.rawValue: @@ -184,12 +191,21 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable { case let .subheader(text): encoder.encodeInt32(InstantPageBlockType.subheader.rawValue, forKey: "r") encoder.encodeObject(text, forKey: "t") + case let .heading(text, level): + encoder.encodeInt32(InstantPageBlockType.heading.rawValue, forKey: "r") + encoder.encodeObject(text, forKey: "t") + encoder.encodeInt32(level, forKey: "l") case let .paragraph(text): encoder.encodeInt32(InstantPageBlockType.paragraph.rawValue, forKey: "r") encoder.encodeObject(text, forKey: "t") - case let .preformatted(text): + case let .preformatted(text, language): encoder.encodeInt32(InstantPageBlockType.preformatted.rawValue, forKey: "r") encoder.encodeObject(text, forKey: "t") + if let language { + encoder.encodeString(language, forKey: "l") + } else { + encoder.encodeNil(forKey: "l") + } case let .footer(text): encoder.encodeInt32(InstantPageBlockType.footer.rawValue, forKey: "r") encoder.encodeObject(text, forKey: "t") @@ -374,14 +390,20 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable { } else { return false } + case let .heading(lhsText, lhsLevel): + if case let .heading(rhsText, rhsLevel) = rhs, lhsText == rhsText, lhsLevel == rhsLevel { + return true + } else { + return false + } case let .paragraph(text): if case .paragraph(text) = rhs { return true } else { return false } - case let .preformatted(text): - if case .preformatted(text) = rhs { + case let .preformatted(lhsText, lhsLanguage): + if case let .preformatted(rhsText, rhsLanguage) = rhs, lhsText == rhsText, lhsLanguage == rhsLanguage { return true } else { return false @@ -545,6 +567,11 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable { throw FlatBuffersError.missingRequiredField() } self = .subheader(try RichText(flatBuffersObject: value.text)) + case .instantpageblockHeading: + guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Heading.self) else { + throw FlatBuffersError.missingRequiredField() + } + self = .heading(text: try RichText(flatBuffersObject: value.text), level: value.level) case .instantpageblockParagraph: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Paragraph.self) else { throw FlatBuffersError.missingRequiredField() @@ -554,7 +581,7 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable { guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Preformatted.self) else { throw FlatBuffersError.missingRequiredField() } - self = .preformatted(try RichText(flatBuffersObject: value.text)) + self = .preformatted(text: try RichText(flatBuffersObject: value.text), language: value.language) case .instantpageblockFooter: guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Footer.self) else { throw FlatBuffersError.missingRequiredField() @@ -698,17 +725,28 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable { let start = TelegramCore_InstantPageBlock_Subheader.startInstantPageBlock_Subheader(&builder) TelegramCore_InstantPageBlock_Subheader.add(text: textOffset, &builder) offset = TelegramCore_InstantPageBlock_Subheader.endInstantPageBlock_Subheader(&builder, start: start) + case let .heading(text, level): + valueType = .instantpageblockHeading + let textOffset = text.encodeToFlatBuffers(builder: &builder) + let start = TelegramCore_InstantPageBlock_Heading.startInstantPageBlock_Heading(&builder) + TelegramCore_InstantPageBlock_Heading.add(text: textOffset, &builder) + TelegramCore_InstantPageBlock_Heading.add(level: level, &builder) + offset = TelegramCore_InstantPageBlock_Heading.endInstantPageBlock_Heading(&builder, start: start) case let .paragraph(text): valueType = .instantpageblockParagraph let textOffset = text.encodeToFlatBuffers(builder: &builder) let start = TelegramCore_InstantPageBlock_Paragraph.startInstantPageBlock_Paragraph(&builder) TelegramCore_InstantPageBlock_Paragraph.add(text: textOffset, &builder) offset = TelegramCore_InstantPageBlock_Paragraph.endInstantPageBlock_Paragraph(&builder, start: start) - case let .preformatted(text): + case let .preformatted(text, language): valueType = .instantpageblockPreformatted let textOffset = text.encodeToFlatBuffers(builder: &builder) + let languageOffset = language.flatMap { builder.create(string: $0) } let start = TelegramCore_InstantPageBlock_Preformatted.startInstantPageBlock_Preformatted(&builder) TelegramCore_InstantPageBlock_Preformatted.add(text: textOffset, &builder) + if let languageOffset { + TelegramCore_InstantPageBlock_Preformatted.add(language: languageOffset, &builder) + } offset = TelegramCore_InstantPageBlock_Preformatted.endInstantPageBlock_Preformatted(&builder, start: start) case let .footer(text): valueType = .instantpageblockFooter diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChatBannedRights.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChatBannedRights.swift index f0ca1387f8..628c94ccd3 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChatBannedRights.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramChatBannedRights.swift @@ -32,6 +32,7 @@ public struct TelegramChatBannedRightsFlags: OptionSet, Hashable { public static let banSendVoice = TelegramChatBannedRightsFlags(rawValue: 1 << 23) public static let banSendFiles = TelegramChatBannedRightsFlags(rawValue: 1 << 24) public static let banSendText = TelegramChatBannedRightsFlags(rawValue: 1 << 25) + public static let banSendReactions = TelegramChatBannedRightsFlags(rawValue: 1 << 27) public static let banEditRank = TelegramChatBannedRightsFlags(rawValue: 1 << 26) } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift index 001fcc1282..3d37e36ab1 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/DeleteMessages.swift @@ -76,6 +76,44 @@ func _internal_deleteAllMessagesWithForwardAuthor(transaction: Transaction, medi } } +func _internal_deleteAllReactionsWithAuthor(account: Account, peerId: PeerId, authorId: PeerId) -> Signal { + return account.postbox.transaction { transaction -> (Peer?, Peer?) in + return (transaction.getPeer(peerId), transaction.getPeer(authorId)) + } + |> mapToSignal { peer, author in + guard let inputPeer = peer.flatMap(apiInputPeer), let inputAuthor = author.flatMap(apiInputPeer) else { + return .complete() + } + return account.network.request(Api.functions.messages.deleteParticipantReactions(peer: inputPeer, participant: inputAuthor)) + |> ignoreValues + |> `catch` { _ -> Signal in + return .complete() + } + } +} + +func _internal_deleteReaction(account: Account, messageId: MessageId, authorId: PeerId) -> Signal { + return account.postbox.transaction { transaction -> (Peer?, Peer?) in + return (transaction.getPeer(messageId.peerId), transaction.getPeer(authorId)) + } + |> mapToSignal { peer, author in + guard let inputPeer = peer.flatMap(apiInputPeer), let inputAuthor = author.flatMap(apiInputPeer) else { + return .complete() + } + return account.network.request(Api.functions.messages.deleteParticipantReaction(peer: inputPeer, msgId: messageId.id, participant: inputAuthor)) + |> map(Optional.init) + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { updates -> Signal in + if let updates { + account.stateManager.addUpdates(updates) + } + return .complete() + } + } +} + func _internal_clearHistory(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, threadId: Int64?, namespaces: MessageIdNamespaces) { if peerId.namespace == Namespaces.Peer.SecretChat { var resourceIds: [MediaResourceId] = [] diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift index 849790ac9a..8670dbab50 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Messages/TelegramEngineMessages.swift @@ -169,6 +169,14 @@ public extension TelegramEngine { } |> ignoreValues } + + public func deleteAllReactionsWithAuthor(peerId: EnginePeer.Id, authorId: EnginePeer.Id) -> Signal { + return _internal_deleteAllReactionsWithAuthor(account: self.account, peerId: peerId, authorId: authorId) + } + + public func deleteReaction(peerId: EnginePeer.Id, messageId: EngineMessage.Id, authorId: EnginePeer.Id) -> Signal { + return _internal_deleteReaction(account: self.account, messageId: messageId, authorId: authorId) + } public func clearCallHistory(forEveryone: Bool) -> Signal { return _internal_clearCallHistory(account: self.account, forEveryone: forEveryone) diff --git a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift index 636318a5c4..5396505457 100644 --- a/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift +++ b/submodules/TelegramUI/Components/AdminUserActionsSheet/Sources/AdminUserActionsSheet.swift @@ -29,6 +29,7 @@ struct MediaRight: OptionSet, Hashable { static let videoMessages = MediaRight(rawValue: 1 << 6) static let links = MediaRight(rawValue: 1 << 7) static let polls = MediaRight(rawValue: 1 << 8) + static let reactions = MediaRight(rawValue: 1 << 9) } extension MediaRight { @@ -75,7 +76,8 @@ private func rightsFromBannedRights(_ rights: TelegramChatBannedRightsFlags) -> .voiceMessages, .videoMessages, .links, - .polls + .polls, + .reactions ] if rights.contains(.banSendText) { @@ -168,6 +170,9 @@ private func rightFlagsFromRights(participantRights: ParticipantRight, mediaRigh if !mediaRights.contains(.polls) { result.insert(.banSendPolls) } + if !mediaRights.contains(.reactions) { + result.insert(.banSendReactions) + } return result } @@ -181,7 +186,8 @@ private let allMediaRightItems: [MediaRight] = [ .voiceMessages, .videoMessages, .links, - .polls + .polls, + .reactions ] private enum AdminUserActionOptionSection { @@ -821,6 +827,8 @@ private final class AdminUserActionsContentComponent: Component { mediaItemTitle = component.strings.Channel_BanUser_PermissionEmbedLinks case .polls: mediaItemTitle = component.strings.Channel_BanUser_PermissionSendPolls + case .reactions: + mediaItemTitle = component.strings.Channel_BanUser_PermissionSendReactions default: continue mediaRightsLoop } diff --git a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift index c9c88568c6..eb5bfb2f56 100644 --- a/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatMessageBubbleItemNode/Sources/ChatMessageBubbleItemNode.swift @@ -2619,14 +2619,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI switch authorRank { case let .creator(rank): if let rank, !rank.isEmpty { - string = rank.trimmingEmojis + string = rank } else { string = item.presentationData.strings.Conversation_Owner } rankBadgeColor = UIColor(rgb: 0x956ac8) case let .admin(rank): if let rank, !rank.isEmpty { - string = rank.trimmingEmojis + string = rank } else { string = item.presentationData.strings.Conversation_Admin } @@ -2637,7 +2637,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI string = item.presentationData.strings.Chat_TagPlaceholder defaultRankColor = defaultRankColor.withMultipliedAlpha(0.5) } else { - string = rank.trimmingEmojis + string = rank } } else { string = "" diff --git a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift index 8edb6af71b..92ecd7dc37 100644 --- a/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift +++ b/submodules/TelegramUI/Components/Chat/ChatTextInputPanelNode/Sources/ChatTextInputPanelNode.swift @@ -2634,7 +2634,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg size: CGSize(width: baseWidth, height: panelHeight) ) - let audioRecordingTimeFrame = CGRect(origin: CGPoint(x: hideOffset.x + leftInset + leftMenuInset + 8.0 + 34.0, y: (accessoryPanel != nil ? 52.0 : 0.0) + panelHeight - minimalHeight + floor((minimalHeight - audioRecordingTimeSize.height) / 2.0) + 1.0 - UIScreenPixel), size: audioRecordingTimeSize) + let audioRecordingTimeFrame = CGRect(origin: CGPoint(x: hideOffset.x + leftInset + leftMenuInset + 8.0 + 22.0, y: (accessoryPanel != nil ? 52.0 : 0.0) + panelHeight - minimalHeight + floor((minimalHeight - audioRecordingTimeSize.height) / 2.0) + 1.0 - UIScreenPixel), size: audioRecordingTimeSize) if animateTimeSlideIn { var previousAudioRecordingTimeFrame = audioRecordingTimeFrame diff --git a/submodules/TelegramUI/Components/ChatParticipantRightsScreen/Sources/ChatParticipantRightsScreen.swift b/submodules/TelegramUI/Components/ChatParticipantRightsScreen/Sources/ChatParticipantRightsScreen.swift index 6bbc4ad256..4e3a5f6995 100644 --- a/submodules/TelegramUI/Components/ChatParticipantRightsScreen/Sources/ChatParticipantRightsScreen.swift +++ b/submodules/TelegramUI/Components/ChatParticipantRightsScreen/Sources/ChatParticipantRightsScreen.swift @@ -260,7 +260,7 @@ private final class ChatParticipantRightsContent: CombinedComponent { state.updated(transition: .easeInOut(duration: 0.2)) }, shouldUpdateText: { [weak state] text in - if text.containsEmoji { + if text.containsGraphicEmoji { state?.animateError() return false } diff --git a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift index c1e232f39c..1a6da87630 100644 --- a/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift +++ b/submodules/TelegramUI/Components/ChatScheduleTimeController/Sources/ChatScheduleTimeScreen.swift @@ -39,6 +39,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component { let currentRepeatPeriod: Int32? let suggestedTime: Int32? let minimalTime: Int32? + let silentPosting: Bool let externalState: ExternalState let dismiss: () -> Void @@ -49,6 +50,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component { currentRepeatPeriod: Int32?, suggestedTime: Int32?, minimalTime: Int32?, + silentPosting: Bool, externalState: ExternalState, dismiss: @escaping () -> Void ) { @@ -58,6 +60,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component { self.currentRepeatPeriod = currentRepeatPeriod self.suggestedTime = suggestedTime self.minimalTime = minimalTime + self.silentPosting = silentPosting self.externalState = externalState self.dismiss = dismiss } @@ -222,6 +225,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component { self.environment = environment if self.component == nil { + self.isSilentPosting = component.silentPosting switch component.mode { case .format, .search: self.minDate = Date(timeIntervalSince1970: 0.0) @@ -561,7 +565,8 @@ private final class ChatScheduleTimeSheetContentComponent: Component { controller.completion( ChatScheduleTimeScreen.Result( time: Int32(self.date?.timeIntervalSince1970 ?? 0), - repeatPeriod: self.repeatPeriod + repeatPeriod: self.repeatPeriod, + silentPosting: self.isSilentPosting ) ) component.dismiss() @@ -609,7 +614,8 @@ private final class ChatScheduleTimeSheetContentComponent: Component { controller.completion( ChatScheduleTimeScreen.Result( time: 0, - repeatPeriod: nil + repeatPeriod: nil, + silentPosting: false ) ) component.dismiss() @@ -650,7 +656,8 @@ private final class ChatScheduleTimeSheetContentComponent: Component { controller.completion( ChatScheduleTimeScreen.Result( time: scheduleWhenOnlineTimestamp, - repeatPeriod: nil + repeatPeriod: nil, + silentPosting: self.isSilentPosting ) ) component.dismiss() @@ -918,6 +925,7 @@ private final class ChatScheduleTimeScreenComponent: Component { let currentRepeatPeriod: Int32? let suggestedTime: Int32? let minimalTime: Int32? + let silentPosting: Bool init( context: AccountContext, @@ -925,7 +933,8 @@ private final class ChatScheduleTimeScreenComponent: Component { currentTime: Int32?, currentRepeatPeriod: Int32?, suggestedTime: Int32?, - minimalTime: Int32? + minimalTime: Int32?, + silentPosting: Bool ) { self.context = context self.mode = mode @@ -933,6 +942,7 @@ private final class ChatScheduleTimeScreenComponent: Component { self.currentRepeatPeriod = currentRepeatPeriod self.suggestedTime = suggestedTime self.minimalTime = minimalTime + self.silentPosting = silentPosting } static func ==(lhs: ChatScheduleTimeScreenComponent, rhs: ChatScheduleTimeScreenComponent) -> Bool { @@ -951,6 +961,9 @@ private final class ChatScheduleTimeScreenComponent: Component { if lhs.minimalTime != rhs.minimalTime { return false } + if lhs.silentPosting != rhs.silentPosting { + return false + } return true } @@ -1005,6 +1018,7 @@ private final class ChatScheduleTimeScreenComponent: Component { currentRepeatPeriod: component.currentRepeatPeriod, suggestedTime: component.suggestedTime, minimalTime: component.minimalTime, + silentPosting: component.silentPosting, externalState: self.contentExternalState, dismiss: { [weak self] in guard let self else { @@ -1080,6 +1094,13 @@ public class ChatScheduleTimeScreen: ViewControllerComponentContainer { public struct Result { public let time: Int32 public let repeatPeriod: Int32? + public let silentPosting: Bool + + public init(time: Int32, repeatPeriod: Int32?, silentPosting: Bool = false) { + self.time = time + self.repeatPeriod = repeatPeriod + self.silentPosting = silentPosting + } } fileprivate let completion: (Result) -> Void @@ -1091,6 +1112,7 @@ public class ChatScheduleTimeScreen: ViewControllerComponentContainer { currentRepeatPeriod: Int32? = nil, suggestedTime: Int32? = nil, minimalTime: Int32? = nil, + silentPosting: Bool = false, isDark: Bool, completion: @escaping (Result) -> Void ) { @@ -1102,7 +1124,8 @@ public class ChatScheduleTimeScreen: ViewControllerComponentContainer { currentTime: currentTime, currentRepeatPeriod: currentRepeatPeriod, suggestedTime: suggestedTime, - minimalTime: minimalTime + minimalTime: minimalTime, + silentPosting: silentPosting ), navigationBarAppearance: .none, theme: isDark ? .dark : .default) self.statusBar.statusBarStyle = .Ignore diff --git a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift index 0bb4a53162..f9e3617e60 100644 --- a/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift +++ b/submodules/TelegramUI/Components/EntityKeyboard/Sources/EmojiPagerContentComponent.swift @@ -1407,6 +1407,7 @@ public final class EmojiPagerContentComponent: Component { private var vibrancyClippingView: UIView private var vibrancyEffectView: UIView? public private(set) var mirrorContentClippingView: UIView? + private let mirrorScrollViewClippingView: UIView private let mirrorContentScrollView: UIView private var warpView: WarpView? private var mirrorContentWarpView: WarpView? @@ -1459,6 +1460,9 @@ public final class EmojiPagerContentComponent: Component { private var longTapRecognizer: UILongPressGestureRecognizer? private func hasSameContentId(_ lhs: ContentId?, _ rhs: ContentId?) -> Bool { + if let rhs, rhs.version < 2 { + return false + } return lhs?.id == rhs?.id } @@ -1480,6 +1484,9 @@ public final class EmojiPagerContentComponent: Component { self.scrollViewClippingView = UIView() self.scrollViewClippingView.clipsToBounds = true + self.mirrorScrollViewClippingView = UIView() + self.mirrorScrollViewClippingView.clipsToBounds = true + self.mirrorContentScrollView = UIView() self.mirrorContentScrollView.layer.anchorPoint = CGPoint() self.mirrorContentScrollView.clipsToBounds = true @@ -1519,6 +1526,8 @@ public final class EmojiPagerContentComponent: Component { self.addSubview(self.scrollViewClippingView) self.scrollViewClippingView.addSubview(self.scrollView) + self.mirrorScrollViewClippingView.addSubview(self.mirrorContentScrollView) + self.scrollView.addSubview(self.placeholdersContainerView) let contextGesture = ContextGesture(target: self, action: #selector(self.tapGesture(_:))) @@ -1658,6 +1667,16 @@ public final class EmojiPagerContentComponent: Component { fatalError("init(coder:) has not been implemented") } + private var mirrorOverlayContainerView: UIView? { + if let mirrorContentClippingView = self.mirrorContentClippingView { + return mirrorContentClippingView + } else if let vibrancyEffectView = self.vibrancyEffectView { + return vibrancyEffectView + } else { + return nil + } + } + func updateIsWarpEnabled(isEnabled: Bool) { if isEnabled { if self.warpView == nil { @@ -1671,6 +1690,7 @@ public final class EmojiPagerContentComponent: Component { let mirrorContentWarpView = WarpView(frame: CGRect()) self.mirrorContentWarpView = mirrorContentWarpView + self.mirrorScrollViewClippingView.addSubview(mirrorContentWarpView) mirrorContentWarpView.contentView.addSubview(self.mirrorContentScrollView) } } else { @@ -1683,12 +1703,7 @@ public final class EmojiPagerContentComponent: Component { if let mirrorContentWarpView = self.mirrorContentWarpView { self.mirrorContentWarpView = nil - if let mirrorContentClippingView = self.mirrorContentClippingView { - mirrorContentClippingView.addSubview(self.mirrorContentScrollView) - } else if let vibrancyEffectView = self.vibrancyEffectView { - vibrancyEffectView.addSubview(self.mirrorContentScrollView) - } - + self.mirrorScrollViewClippingView.addSubview(self.mirrorContentScrollView) mirrorContentWarpView.removeFromSuperview() } } @@ -4117,12 +4132,11 @@ public final class EmojiPagerContentComponent: Component { mirrorContentClippingView = UIView() mirrorContentClippingView.clipsToBounds = false self.mirrorContentClippingView = mirrorContentClippingView - - if let mirrorContentWarpView = self.mirrorContentWarpView { - mirrorContentClippingView.addSubview(mirrorContentWarpView) - } else { - mirrorContentClippingView.addSubview(self.mirrorContentScrollView) - } + } + if self.mirrorScrollViewClippingView.superview !== mirrorContentClippingView { + mirrorContentClippingView.insertSubview(self.mirrorScrollViewClippingView, at: 0) + } else { + mirrorContentClippingView.sendSubviewToBack(self.mirrorScrollViewClippingView) } let clippingFrame = CGRect(origin: CGPoint(x: 0.0, y: pagerEnvironment.containerInsets.top), size: CGSize(width: backgroundFrame.width, height: backgroundFrame.height)) @@ -4146,9 +4160,13 @@ public final class EmojiPagerContentComponent: Component { } self.vibrancyEffectView = vibrancyEffectView self.backgroundTintView.mask = vibrancyEffectView - self.vibrancyClippingView.addSubview(self.mirrorContentScrollView) vibrancyEffectView.addSubview(self.vibrancyClippingView) } + if self.mirrorScrollViewClippingView.superview !== self.vibrancyClippingView { + self.vibrancyClippingView.insertSubview(self.mirrorScrollViewClippingView, at: 0) + } else { + self.vibrancyClippingView.sendSubviewToBack(self.mirrorScrollViewClippingView) + } } if component.hideBackground { @@ -4551,6 +4569,9 @@ public final class EmojiPagerContentComponent: Component { transition.setFrame(view: self.vibrancyClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? clippingTopInset : 0.0), size: availableSize)) transition.setBounds(view: self.vibrancyClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? clippingTopInset : 0.0), size: availableSize)) + transition.setFrame(view: self.mirrorScrollViewClippingView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? clippingTopInset : 0.0), size: availableSize)) + transition.setBounds(view: self.mirrorScrollViewClippingView, bounds: CGRect(origin: CGPoint(x: 0.0, y: self.isSearchActivated ? clippingTopInset : 0.0), size: availableSize)) + let previousSize = self.scrollView.bounds.size self.scrollView.bounds = CGRect(origin: self.scrollView.bounds.origin, size: scrollSize) @@ -4714,11 +4735,9 @@ public final class EmojiPagerContentComponent: Component { if self.isSearchActivated { if visibleSearchHeader.superview != self { self.addSubview(visibleSearchHeader) - if self.mirrorContentClippingView != nil { - self.mirrorContentClippingView?.addSubview(visibleSearchHeader.tintContainerView) - } else { - self.mirrorContentScrollView.superview?.superview?.addSubview(visibleSearchHeader.tintContainerView) - } + } + if let mirrorOverlayContainerView = self.mirrorOverlayContainerView, visibleSearchHeader.tintContainerView.superview !== mirrorOverlayContainerView { + mirrorOverlayContainerView.addSubview(visibleSearchHeader.tintContainerView) } } else { /*if useOpaqueTheme { @@ -4779,7 +4798,7 @@ public final class EmojiPagerContentComponent: Component { self.visibleSearchHeader = visibleSearchHeader if self.isSearchActivated { self.addSubview(visibleSearchHeader) - self.mirrorContentClippingView?.addSubview(visibleSearchHeader.tintContainerView) + self.mirrorOverlayContainerView?.addSubview(visibleSearchHeader.tintContainerView) } else { self.scrollView.addSubview(visibleSearchHeader) self.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView) diff --git a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift index c2eb599ab8..7561222c9b 100644 --- a/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift +++ b/submodules/TelegramUI/Components/Gifts/GiftViewScreen/Sources/GiftViewScreen.swift @@ -620,7 +620,7 @@ private final class GiftViewSheetContent: CombinedComponent { title: presentationData.strings.Gift_Convert_Title, text: text, actions: [ - TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Gift_Convert_Convert, action: { [weak self, weak controller, weak navigationController] in guard let self else { return diff --git a/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift b/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift index 1b55e61013..0eb250bf4d 100644 --- a/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift +++ b/submodules/TelegramUI/Components/LegacyCamera/Sources/LegacyCamera.swift @@ -11,7 +11,7 @@ import ShareController import LegacyUI import LegacyMediaPickerUI -public func presentedLegacyCamera(context: AccountContext, peer: Peer?, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, attachmentController: ViewController? = nil, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: NSAttributedString, hasSchedule: Bool, enablePhoto: Bool, enableVideo: Bool, sendPaidMessageStars: Int64 = 0, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ChatSendMessageActionSheetController.SendParameters?) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, dismissedWithResult: @escaping () -> Void = {}, finishedTransitionIn: @escaping () -> Void = {}) { +public func presentedLegacyCamera(context: AccountContext, peer: Peer?, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, attachmentController: ViewController? = nil, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: NSAttributedString, hasSchedule: Bool, enablePhoto: Bool, enableVideo: Bool, sendPaidMessageStars: Int64 = 0, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ChatSendMessageActionSheetController.SendParameters?) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32, Bool) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, dismissedWithResult: @escaping () -> Void = {}, finishedTransitionIn: @escaping () -> Void = {}) { let presentationData = context.sharedContext.currentPresentationData.with { $0 } let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait) @@ -45,8 +45,8 @@ public func presentedLegacyCamera(context: AccountContext, peer: Peer?, chatLoca } controller.presentScheduleController = { _, done in - presentSchedulePicker(true, { time in - done?(time) + presentSchedulePicker(true, { time, silentPosting in + done?(time, silentPosting) }) } controller.presentTimerController = { done in diff --git a/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift b/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift index c856324ebd..8e8c81eb1d 100644 --- a/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift +++ b/submodules/TelegramUI/Components/LegacyInstantVideoController/Sources/LegacyInstantVideoController.swift @@ -133,7 +133,7 @@ public func legacyInputMicPalette(from theme: PresentationTheme) -> TGModernConv return TGModernConversationInputMicPallete(dark: theme.overallDarkAppearance, buttonColor: inputPanelTheme.actionControlFillColor, iconColor: inputPanelTheme.actionControlForegroundColor, backgroundColor: theme.rootController.navigationBar.opaqueBackgroundColor, borderColor: inputPanelTheme.panelSeparatorColor, lock: inputPanelTheme.panelControlAccentColor, textColor: inputPanelTheme.primaryTextColor, secondaryTextColor: inputPanelTheme.secondaryTextColor, recording: inputPanelTheme.mediaRecordingDotColor) } -public func legacyInstantVideoController(theme: PresentationTheme, forStory: Bool, panelFrame: CGRect, context: AccountContext, peerId: PeerId, slowmodeState: ChatSlowmodeState?, hasSchedule: Bool, send: @escaping (InstantVideoController, EnqueueMessage?) -> Void, displaySlowmodeTooltip: @escaping (UIView, CGRect) -> Void, presentSchedulePicker: @escaping (@escaping (Int32) -> Void) -> Void) -> InstantVideoController { +public func legacyInstantVideoController(theme: PresentationTheme, forStory: Bool, panelFrame: CGRect, context: AccountContext, peerId: PeerId, slowmodeState: ChatSlowmodeState?, hasSchedule: Bool, send: @escaping (InstantVideoController, EnqueueMessage?) -> Void, displaySlowmodeTooltip: @escaping (UIView, CGRect) -> Void, presentSchedulePicker: @escaping (@escaping (Int32, Bool) -> Void) -> Void) -> InstantVideoController { let isSecretChat = peerId.namespace == Namespaces.Peer.SecretChat let legacyController = InstantVideoController(presentation: .custom, theme: theme) @@ -166,8 +166,8 @@ public func legacyInstantVideoController(theme: PresentationTheme, forStory: Boo return node }, canSendSilently: !isSecretChat, canSchedule: hasSchedule, reminder: peerId == context.account.peerId)! controller.presentScheduleController = { done in - presentSchedulePicker { time in - done?(time) + presentSchedulePicker { time, silentPosting in + done?(time, silentPosting) } } controller.finishedWithVideo = { [weak legacyController] videoUrl, previewImage, _, duration, dimensions, liveUploadData, adjustments, isSilent, scheduleTimestamp in diff --git a/submodules/TelegramUI/Components/NotificationPeerExceptionController/Sources/NotificationPeerExceptionController.swift b/submodules/TelegramUI/Components/NotificationPeerExceptionController/Sources/NotificationPeerExceptionController.swift index c9fcea3e0a..0a4c7ffdcc 100644 --- a/submodules/TelegramUI/Components/NotificationPeerExceptionController/Sources/NotificationPeerExceptionController.swift +++ b/submodules/TelegramUI/Components/NotificationPeerExceptionController/Sources/NotificationPeerExceptionController.swift @@ -1116,7 +1116,7 @@ public func notificationPeerExceptionController( break } }), - TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: { + TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: { }) ], parseMarkdown: true), in: .window(.root)) } diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift index 62c731b5fe..40e9900476 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoScreen/Sources/PeerInfoScreen.swift @@ -4533,7 +4533,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro TextAlertAction(type: .destructiveAction, title: actionText, action: { self?.deletePeerChat(peer: peer._asPeer(), globally: delete) }), - TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { }) ], parseMarkdown: true), in: .window(.root)) }) diff --git a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift index 9f1663f763..e1ad235d1c 100644 --- a/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift +++ b/submodules/TelegramUI/Components/PeerInfo/PeerInfoVisualMediaPaneNode/Sources/PeerInfoStoryPaneNode.swift @@ -5177,7 +5177,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr public func presentDeleteBotPreviewLanguage() { self.parentController?.present(textAlertController(context: self.context, title: self.presentationData.strings.BotPreviews_DeleteTranslationAlert_Title, text: self.presentationData.strings.BotPreviews_DeleteTranslationAlert_Text, actions: [ - TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: { + TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .destructiveAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in guard let self else { diff --git a/submodules/TelegramUI/Components/ShareExtensionContext/BUILD b/submodules/TelegramUI/Components/ShareExtensionContext/BUILD index 47e6c76e66..1b1e2be9d1 100644 --- a/submodules/TelegramUI/Components/ShareExtensionContext/BUILD +++ b/submodules/TelegramUI/Components/ShareExtensionContext/BUILD @@ -33,6 +33,7 @@ swift_library( "//submodules/ActivityIndicator", "//submodules/DebugSettingsUI", "//submodules/ManagedFile", + "//submodules/ContextUI", "//submodules/TelegramUI/Components/TelegramUIDeclareEncodables", "//submodules/TelegramUI/Components/AnimationCache", "//submodules/TelegramUI/Components/MultiAnimationRenderer", @@ -41,6 +42,7 @@ swift_library( "//submodules/TelegramUI/Components/TelegramAccountAuxiliaryMethods", "//submodules/TelegramUI/Components/PeerSelectionController", "//submodules/TelegramUI/Components/ContextMenuScreen", + "//submodules/TelegramUI/Components/ContextControllerImpl", "//submodules/TelegramUI/Components/NavigationBarImpl", ], visibility = [ diff --git a/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift b/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift index c3b242c393..9ee724f9b4 100644 --- a/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift +++ b/submodules/TelegramUI/Components/ShareExtensionContext/Sources/ShareExtensionContext.swift @@ -34,6 +34,8 @@ import TelegramAccountAuxiliaryMethods import PeerSelectionController import ContextMenuScreen import NavigationBarImpl +import ContextUI +import ContextControllerImpl private var installedSharedLogger = false @@ -201,6 +203,18 @@ public class ShareRootControllerImpl { defaultNavigationBarImpl = { presentationData in return NavigationBarImpl(presentationData: presentationData) } + makeContextControllerImpl = { context, presentationData, configuration, recognizer, gesture, workaroundUseLegacyImplementation, disableScreenshots, hideReactionPanelTail in + return ContextControllerImpl( + context: context, + presentationData: presentationData, + configuration: configuration, + recognizer: recognizer, + gesture: gesture, + workaroundUseLegacyImplementation: workaroundUseLegacyImplementation, + disableScreenshots: disableScreenshots, + hideReactionPanelTail: hideReactionPanelTail + ) + } } deinit { diff --git a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift index e374ef17e8..90a57d5a3e 100644 --- a/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift +++ b/submodules/TelegramUI/Components/Stories/StoryContainerScreen/Sources/StoryItemSetContainerViewSendMessage.swift @@ -947,7 +947,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { }) } - func performSendContextResultAction(view: StoryItemSetContainerComponent.View, results: ChatContextResultCollection, result: ChatContextResult) { + func performSendContextResultAction(view: StoryItemSetContainerComponent.View, results: ChatContextResultCollection, result: ChatContextResult, silentPosting: Bool = false, scheduleTime: Int32? = nil) { guard let component = view.component else { return } @@ -987,6 +987,8 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { replyTo: nil, storyId: focusedStoryId, content: .contextResult(results, result), + silentPosting: silentPosting, + scheduleTime: scheduleTime, sendPaidMessageStars: component.slice.additionalPeerData.sendPaidMessageStars ) |> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in Queue.mainQueue().after(0.3) { @@ -1140,8 +1142,8 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let self, let view else { return } - self.presentScheduleTimePicker(view: view, peer: peer, completion: { time, repeatPeriod in - done(time) + self.presentScheduleTimePicker(view: view, peer: peer, completion: { time, repeatPeriod, silentPosting in + done(time, silentPosting) }) }))) } @@ -2292,8 +2294,8 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let self, let view else { return } - self.presentScheduleTimePicker(view: view, peer: peer, style: media ? .media : .default, completion: { time, repeatPeriod in - done(time) + self.presentScheduleTimePicker(view: view, peer: peer, style: media ? .media : .default, completion: { time, repeatPeriod, silentPosting in + done(time, silentPosting) }) } controller.presentTimerPicker = { [weak self, weak view] done in @@ -2372,7 +2374,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { if let view, let component = view.component { let theme = component.theme let updatedPresentationData: (initial: PresentationData, signal: Signal) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) - let controller = WebSearchController(context: component.context, updatedPresentationData: updatedPresentationData, peer: peer, chatLocation: .peer(id: peer.id), configuration: searchBotsConfiguration, mode: .media(attachment: false, completion: { [weak view] results, selectionState, editingState, silentPosting in + let controller = WebSearchController(context: component.context, updatedPresentationData: updatedPresentationData, peer: peer, chatLocation: .peer(id: peer.id), configuration: searchBotsConfiguration, mode: .media(attachment: false, completion: { [weak view] results, selectionState, editingState, silentPosting, scheduleTime in if let legacyController = legacyController { legacyController.dismiss() } @@ -2381,14 +2383,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak view] result in if let strongSelf = self, let view { - strongSelf.enqueueChatContextResult(view: view, peer: peer, replyMessageId: replyMessageId, storyId: replyToStoryId, results: results, result: result, hideVia: true) + strongSelf.enqueueChatContextResult(view: view, peer: peer, replyMessageId: replyMessageId, storyId: replyToStoryId, results: results, result: result, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime) } }, enqueueMediaMessages: { [weak view] signals in if let strongSelf = self, let view { if editingMedia { strongSelf.editMessageMediaWithLegacySignals(view: view, signals: signals) } else { - strongSelf.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: replyMessageId, replyToStoryId: replyToStoryId, signals: signals, silentPosting: silentPosting) + strongSelf.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: replyMessageId, replyToStoryId: replyToStoryId, signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime) } } }) @@ -2414,8 +2416,8 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { component.controller()?.present(textAlertController(context: component.context, updatedPresentationData: (presentationData, .single(presentationData)), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root)) }, presentSchedulePicker: { [weak view] media, done in if let strongSelf = self, let view { - strongSelf.presentScheduleTimePicker(view: view, peer: peer, style: media ? .media : .default, completion: { time, repeatPeriod in - done(time) + strongSelf.presentScheduleTimePicker(view: view, peer: peer, style: media ? .media : .default, completion: { time, repeatPeriod, silentPosting in + done(time, silentPosting) }) } }, presentTimerPicker: { [weak view] done in @@ -2640,7 +2642,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { // component.controller()?.push(controller) } - private func enqueueChatContextResult(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyMessageId: EngineMessage.Id?, storyId: StoryId?, results: ChatContextResultCollection, result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, resetTextInputState: Bool = true) { + private func enqueueChatContextResult(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyMessageId: EngineMessage.Id?, storyId: StoryId?, results: ChatContextResultCollection, result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, scheduleTime: Int32? = nil, resetTextInputState: Bool = true) { if !canSendMessagesToPeer(peer._asPeer()) { return } @@ -2669,7 +2671,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { } } - sendMessage(nil) + sendMessage(scheduleTime) } private func presentWebSearch(view: StoryItemSetContainerComponent.View, activateOnDisplay: Bool = true, present: @escaping (ViewController, Any?) -> Void) { @@ -2686,14 +2688,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) |> deliverOnMainQueue).start(next: { [weak self, weak view] configuration in if let self { - let controller = WebSearchController(context: context, updatedPresentationData: updatedPresentationData, peer: peer, chatLocation: .peer(id: peer.id), configuration: configuration, mode: .media(attachment: true, completion: { [weak self] results, selectionState, editingState, silentPosting in + let controller = WebSearchController(context: context, updatedPresentationData: updatedPresentationData, peer: peer, chatLocation: .peer(id: peer.id), configuration: configuration, mode: .media(attachment: true, completion: { [weak self] results, selectionState, editingState, silentPosting, scheduleTime in legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak self, weak view] result in if let self, let view { - self.performSendContextResultAction(view: view, results: results, result: result) + self.performSendContextResultAction(view: view, results: results, result: result, silentPosting: silentPosting, scheduleTime: scheduleTime) } }, enqueueMediaMessages: { [weak self, weak view] signals in if let self, let view, !signals.isEmpty { - self.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: nil, replyToStoryId: StoryId(peerId: peer.id, id: storyId), signals: signals, silentPosting: false) + self.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: nil, replyToStoryId: StoryId(peerId: peer.id, id: storyId), signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime) } }) }), activateOnDisplay: activateOnDisplay) @@ -2833,8 +2835,8 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { guard let self, let view else { return } - self.presentScheduleTimePicker(view: view, peer: peer, style: .media, completion: { time, repeatPeriod in - done(time) + self.presentScheduleTimePicker(view: view, peer: peer, style: .media, completion: { time, repeatPeriod, silentPosting in + done(time, silentPosting) }) }, presentTimerPicker: { [weak self, weak view] done in guard let self, let view else { @@ -2868,7 +2870,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { style: ChatScheduleTimeControllerStyle = .default, selectedTime: Int32? = nil, dismissByTapOutside: Bool = true, - completion: @escaping (Int32, Int32?) -> Void + completion: @escaping (Int32, Int32?, Bool) -> Void ) { guard let component = view.component else { return @@ -2889,16 +2891,25 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) { sendWhenOnlineAvailable = false } - let mode: ChatScheduleTimeControllerMode + let mode: ChatScheduleTimeScreen.Mode //ChatScheduleTimeControllerMode if peer.id == component.context.account.peerId { mode = .reminders } else { - mode = .scheduledMessages(sendWhenOnlineAvailable: sendWhenOnlineAvailable) + mode = .scheduledMessages(peerId: peer.id, sendWhenOnlineAvailable: sendWhenOnlineAvailable) } - let theme = component.theme - let controller = ChatScheduleTimeController(context: component.context, updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }), mode: mode, style: style, currentTime: selectedTime, minimalTime: nil, dismissByTapOutside: dismissByTapOutside, completion: { time in - completion(time, nil) - }) + let controller = ChatScheduleTimeScreen( + context: component.context, + mode: mode, + currentTime: selectedTime, + currentRepeatPeriod: nil, + suggestedTime: nil, + minimalTime: nil, + silentPosting: false, + isDark: style == .media, + completion: { result in + completion(result.time, nil, result.silentPosting) + } + ) view.endEditing(true) view.component?.controller()?.present(controller, in: .window(.root)) }) diff --git a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift index 28039854e9..8e663ee071 100644 --- a/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift +++ b/submodules/TelegramUI/Components/VideoMessageCameraScreen/Sources/VideoMessageCameraScreen.swift @@ -441,7 +441,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent { } }, error: { [weak self] _ in if let self, let controller = self.getController() { - controller.completion(nil, nil, nil) + controller.completion(nil, nil, nil, nil) } })) } @@ -1641,7 +1641,7 @@ public class VideoMessageCameraScreen: ViewController { fileprivate var allowLiveUpload: Bool fileprivate var viewOnceAvailable: Bool - fileprivate let completion: (EnqueueMessage?, Bool?, Int32?) -> Void + fileprivate let completion: (EnqueueMessage?, Bool?, Int32?, Int32?) -> Void private var audioSessionDisposable: Disposable? @@ -1794,7 +1794,7 @@ public class VideoMessageCameraScreen: ViewController { viewOnceAvailable: Bool, inputPanelFrame: (CGRect, Bool), chatNode: ASDisplayNode?, - completion: @escaping (EnqueueMessage?, Bool?, Int32?) -> Void + completion: @escaping (EnqueueMessage?, Bool?, Int32?, Int32?) -> Void ) { self.context = context self.updatedPresentationData = updatedPresentationData @@ -1833,7 +1833,7 @@ public class VideoMessageCameraScreen: ViewController { fileprivate var didSend = false fileprivate var lastActionTimestamp: Double? fileprivate var isSendingImmediately = false - public func sendVideoRecording(silentPosting: Bool? = nil, scheduleTime: Int32? = nil, messageEffect: ChatSendMessageEffect? = nil) { + public func sendVideoRecording(silentPosting: Bool? = nil, scheduleTime: Int32? = nil, repeatPeriod: Int32? = nil, messageEffect: ChatSendMessageEffect? = nil) { guard !self.didSend else { return } @@ -1845,7 +1845,7 @@ public class VideoMessageCameraScreen: ViewController { } if case .none = self.cameraState.recording, self.node.results.isEmpty { - self.completion(nil, nil, nil) + self.completion(nil, nil, nil, nil) return } @@ -1859,7 +1859,7 @@ public class VideoMessageCameraScreen: ViewController { self.waitingForNextResult = true self.node.stopRecording.invoke(Void()) } else { - self.completion(nil, nil, nil) + self.completion(nil, nil, nil, nil) return } } @@ -1889,7 +1889,7 @@ public class VideoMessageCameraScreen: ViewController { } if duration < 1.0 { - self.completion(nil, nil, nil) + self.completion(nil, nil, nil, nil) return } @@ -1998,7 +1998,7 @@ public class VideoMessageCameraScreen: ViewController { localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [] - ), silentPosting, scheduleTime) + ), silentPosting, scheduleTime, repeatPeriod) }) }) } diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerEditGif.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerEditGif.swift index 07011b5379..741427088e 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerEditGif.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerEditGif.swift @@ -35,12 +35,12 @@ extension ChatControllerImpl { guard let self else { return } - self.presentScheduleTimePicker(style: .media, completion: { [weak self] time, _ in + self.presentScheduleTimePicker(style: .media, completion: { [weak self] result in guard let self else { return } - done(time) - if self.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + done(result.time, result.silentPosting) + if self.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp { self.openScheduledMessages() } }) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift index bf6c235c20..61611b0b0a 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerLoadDisplayNode.swift @@ -1007,7 +1007,8 @@ extension ChatControllerImpl { } } - let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: silentPosting ?? false, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod, postpone: postpone) + let effectiveSilentPosting = silentPosting ?? strongSelf.presentationInterfaceState.interfaceState.silentPosting + let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: effectiveSilentPosting, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod, postpone: postpone) var forwardedMessages: [[EnqueueMessage]] = [] var forwardSourcePeerIds = Set() @@ -1962,7 +1963,7 @@ extension ChatControllerImpl { } self.beginDeleteMessagesWithUndo(messageIds: Set(messages.map({ $0.id })), type: .forEveryone) }), - TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {}) + TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}) ], actionLayout: .vertical ) diff --git a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift index ba6084fba9..57ba2caca7 100644 --- a/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift +++ b/submodules/TelegramUI/Sources/Chat/ChatControllerMediaRecording.swift @@ -182,7 +182,7 @@ extension ChatControllerImpl { viewOnceAvailable: viewOnceAvailable, inputPanelFrame: (currentInputPanelFrame, self.chatDisplayNode.inputNode != nil), chatNode: self.chatDisplayNode.historyNode, - completion: { [weak self] message, silentPosting, scheduleTime in + completion: { [weak self] message, silentPosting, scheduleTime, repeatPeriod in guard let self, let videoController = self.videoRecorderValue else { return } @@ -230,14 +230,8 @@ extension ChatControllerImpl { }, usedCorrelationId ? correlationId : nil) let messages = [message] - let transformedMessages: [EnqueueMessage] - if let silentPosting { - transformedMessages = self.transformEnqueueMessages(messages, silentPosting: silentPosting) - } else if let scheduleTime { - transformedMessages = self.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime) - } else { - transformedMessages = self.transformEnqueueMessages(messages) - } + let effectiveSilentPosting = silentPosting ?? self.presentationInterfaceState.interfaceState.silentPosting + let transformedMessages = self.transformEnqueueMessages(messages, silentPosting: effectiveSilentPosting, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod) self.sendMessages(transformedMessages) } @@ -758,14 +752,8 @@ extension ChatControllerImpl { let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(audio.fileSize), attributes: [.Audio(isVoice: true, duration: finalDuration, title: nil, performer: nil, waveform: waveformBuffer)], alternativeRepresentations: [])), threadId: self.chatLocation.threadId, replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])] - let transformedMessages: [EnqueueMessage] - if let silentPosting = silentPosting { - transformedMessages = self.transformEnqueueMessages(messages, silentPosting: silentPosting, postpone: postpone) - } else if let scheduleTime = scheduleTime { - transformedMessages = self.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod, postpone: postpone) - } else { - transformedMessages = self.transformEnqueueMessages(messages) - } + let effectiveSilentPosting = silentPosting ?? self.presentationInterfaceState.interfaceState.silentPosting + let transformedMessages = self.transformEnqueueMessages(messages, silentPosting: effectiveSilentPosting, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod, postpone: postpone) guard let peerId = self.chatLocation.peerId else { return @@ -783,7 +771,7 @@ extension ChatControllerImpl { guard let videoRecorderValue = self.videoRecorderValue else { return } - videoRecorderValue.sendVideoRecording(silentPosting: silentPosting, scheduleTime: scheduleTime, messageEffect: messageEffect) + videoRecorderValue.sendVideoRecording(silentPosting: silentPosting, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod, messageEffect: messageEffect) } } } diff --git a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift index acf7b004a7..c508e9bce4 100644 --- a/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift +++ b/submodules/TelegramUI/Sources/Chat/UpdateChatPresentationInterfaceState.swift @@ -94,7 +94,7 @@ func updateChatPresentationInterfaceStateImpl( guard let selfController, value else { return } - selfController.present(textAlertController(context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: selfController.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .defaultAction, title: selfController.presentationData.strings.Common_Cancel, action: { + selfController.present(textAlertController(context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: selfController.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .genericAction, title: selfController.presentationData.strings.Common_Cancel, action: { }), TextAlertAction(type: .defaultAction, title: selfController.presentationData.strings.Common_OK, action: { [weak selfController] in guard let selfController else { return @@ -151,7 +151,7 @@ func updateChatPresentationInterfaceStateImpl( case .generic: break case let .inlineBotLocationRequest(peerId): - selfController.present(textAlertController(context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: selfController.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .defaultAction, title: selfController.presentationData.strings.Common_Cancel, action: { [weak selfController] in + selfController.present(textAlertController(context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: selfController.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .genericAction, title: selfController.presentationData.strings.Common_Cancel, action: { [weak selfController] in guard let selfController else { return } diff --git a/submodules/TelegramUI/Sources/ChatController.swift b/submodules/TelegramUI/Sources/ChatController.swift index 90bfead591..0efc6cbbe2 100644 --- a/submodules/TelegramUI/Sources/ChatController.swift +++ b/submodules/TelegramUI/Sources/ChatController.swift @@ -2387,9 +2387,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } }, shouldAnimateMessageTransition ? correlationId : nil) - strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime, repeatPeriod in + strongSelf.presentScheduleTimePicker(completion: { [weak self] result in if let strongSelf = self { - let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod, postpone: postpone) + let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: result.silentPosting, scheduleTime: result.time, repeatPeriod: result.repeatPeriod, postpone: postpone) strongSelf.sendMessages(transformedMessages) } }) @@ -2557,9 +2557,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G messages = strongSelf.transformEnqueueMessages(messages, silentPosting: true) strongSelf.sendMessages(messages) } else if schedule { - strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime, repeatPeriod in + strongSelf.presentScheduleTimePicker(completion: { [weak self] result in if let strongSelf = self { - let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod) + let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: result.silentPosting, scheduleTime: result.time, repeatPeriod: result.repeatPeriod) strongSelf.sendMessages(transformedMessages) } }) @@ -4031,15 +4031,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G guard !self.presentAccountFrozenInfoIfNeeded(delay: true) else { return } - self.presentScheduleTimePicker(completion: { [weak self] time, repeatPeriod in + self.presentScheduleTimePicker(completion: { [weak self] result in if let strongSelf = self { if let _ = strongSelf.presentationInterfaceState.interfaceState.mediaDraftState { - strongSelf.sendMediaRecording(scheduleTime: time, messageEffect: (params?.effect).flatMap { + strongSelf.sendMediaRecording(silentPosting: result.silentPosting, scheduleTime: result.time, repeatPeriod: result.repeatPeriod, messageEffect: (params?.effect).flatMap { return ChatSendMessageEffect(id: $0.id) }) } else { - let silentPosting = strongSelf.presentationInterfaceState.interfaceState.silentPosting - strongSelf.chatDisplayNode.sendCurrentMessage(silentPosting: silentPosting, scheduleTime: time, repeatPeriod: repeatPeriod, messageEffect: (params?.effect).flatMap { + strongSelf.chatDisplayNode.sendCurrentMessage(silentPosting: result.silentPosting, scheduleTime: result.time, repeatPeriod: result.repeatPeriod, messageEffect: (params?.effect).flatMap { return ChatSendMessageEffect(id: $0.id) }) { [weak self] in if let strongSelf = self { @@ -4047,7 +4046,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G $0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) } }) - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp { strongSelf.openScheduledMessages() } } @@ -8679,9 +8678,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G }) } } else { - self.presentScheduleTimePicker(style: media ? .media : .default, dismissByTapOutside: false, completion: { [weak self] time, repeatPeriod in + self.presentScheduleTimePicker(style: media ? .media : .default, dismissByTapOutside: false, completion: { [weak self] result in if let strongSelf = self { - strongSelf.sendMessages(strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: time, repeatPeriod: repeatPeriod, postpone: postpone), commit: true) + strongSelf.sendMessages(strongSelf.transformEnqueueMessages(messages, silentPosting: result.silentPosting, scheduleTime: result.time, repeatPeriod: result.repeatPeriod, postpone: postpone), commit: true) } }) } @@ -8941,7 +8940,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G })) } - func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, resetTextInputState: Bool = true, postpone: Bool = false) { + func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, scheduleTime: Int32? = nil, resetTextInputState: Bool = true, postpone: Bool = false) { if !canSendMessagesToChat(self.presentationInterfaceState) { return } @@ -8955,7 +8954,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G isScheduledMessages = true } - let sendMessage: (Int32?) -> Void = { [weak self] scheduleTime in + let sendMessage: (Int32?, Bool) -> Void = { [weak self] scheduleTime, silentPosting in guard let self else { return } @@ -8991,12 +8990,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } } - if isScheduledMessages { - self.presentScheduleTimePicker(style: .default, dismissByTapOutside: false, completion: { time, repeatPeriod in - sendMessage(time) + if let scheduleTime { + sendMessage(scheduleTime, silentPosting) + } else if isScheduledMessages { + self.presentScheduleTimePicker(style: .default, dismissByTapOutside: false, completion: { result in + sendMessage(result.time, result.silentPosting) }) } else { - sendMessage(nil) + sendMessage(nil, silentPosting) } } @@ -10249,6 +10250,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G } func presentScheduleTimePicker(style: ChatScheduleTimeControllerStyle = .default, selectedTime: Int32? = nil, selectedRepeatPeriod: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32, Int32?) -> Void) { + self.presentScheduleTimePicker(style: style, selectedTime: selectedTime, selectedRepeatPeriod: selectedRepeatPeriod, dismissByTapOutside: dismissByTapOutside, completion: { result in + completion(result.time, result.repeatPeriod) + }) + } + + func presentScheduleTimePicker(style: ChatScheduleTimeControllerStyle = .default, selectedTime: Int32? = nil, selectedRepeatPeriod: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (ChatScheduleTimeScreen.Result) -> Void) { guard let peerId = self.chatLocation.peerId else { return } @@ -10279,9 +10286,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G currentTime: selectedTime, currentRepeatPeriod: selectedRepeatPeriod, minimalTime: strongSelf.presentationInterfaceState.slowmodeState?.timeout, + silentPosting: strongSelf.presentationInterfaceState.interfaceState.silentPosting, isDark: style == .media, completion: { result in - completion(result.time, result.repeatPeriod) + completion(result) } ) strongSelf.chatDisplayNode.dismissInput() diff --git a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift index 3e43210c9a..81df672615 100644 --- a/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift +++ b/submodules/TelegramUI/Sources/ChatControllerAdminBanUsers.swift @@ -21,7 +21,7 @@ fileprivate struct InitialBannedRights { } extension ChatControllerImpl { - fileprivate func applyAdminUserActionsResult(messageIds: Set, result: AdminUserActionsSheet.ChatResult, initialUserBannedRights: [EnginePeer.Id: InitialBannedRights]) { + fileprivate func applyAdminUserActionsResult(messageIds: Set, reactionPeerId: EnginePeer.Id? = nil, result: AdminUserActionsSheet.ChatResult, initialUserBannedRights: [EnginePeer.Id: InitialBannedRights]) { guard let messagesPeerId = self.chatLocation.peerId else { return } @@ -30,7 +30,12 @@ extension ChatControllerImpl { } - var title: String? = messageIds.count == 1 ? self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTitleSingle : self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTitleMultiple + var title: String? + if let _ = reactionPeerId { + title = self.presentationData.strings.Chat_AdminAction_ToastReactionsDeletedTitleSingle + } else { + title = messageIds.count == 1 ? self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTitleSingle : self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTitleMultiple + } if !result.deleteAllFromPeers.isEmpty { title = self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTitleMultiple } @@ -74,6 +79,10 @@ extension ChatControllerImpl { let _ = self.context.engine.messages.clearAuthorHistory(peerId: messagesPeerId, memberId: authorId).startStandalone() } + for authorId in result.deleteAllReactionsFromPeers { + let _ = self.context.engine.messages.deleteAllReactionsWithAuthor(peerId: messagesPeerId, authorId: authorId).startStandalone() + } + for authorId in result.reportSpamPeers { let _ = self.context.engine.peers.reportPeer(peerId: authorId, reason: .spam, message: "").startStandalone() } @@ -88,9 +97,17 @@ extension ChatControllerImpl { } if text.isEmpty { - text = messageIds.count == 1 ? self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextSingle : self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextMultiple - if !result.deleteAllFromPeers.isEmpty { + if let _ = reactionPeerId { + text = self.presentationData.strings.Chat_AdminAction_ToastReactionsDeletedTextSingle + } else { + text = messageIds.count == 1 ? self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextSingle : self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextMultiple + } + if !result.deleteAllFromPeers.isEmpty && !result.deleteAllReactionsFromPeers.isEmpty { + text = self.presentationData.strings.Chat_AdminAction_ToastMessagesAndReactionsDeletedText + } else if !result.deleteAllFromPeers.isEmpty { text = self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextMultiple + } else if !result.deleteAllReactionsFromPeers.isEmpty { + text = self.presentationData.strings.Chat_AdminAction_ToastReactionsDeletedTextMultiple } title = nil } @@ -332,7 +349,7 @@ extension ChatControllerImpl { guard let self else { return } - self.applyAdminUserActionsResult(messageIds: messageIds, result: result, initialUserBannedRights: initialUserBannedRights) + self.applyAdminUserActionsResult(messageIds: messageIds, reactionPeerId: authorPeer.id, result: result, initialUserBannedRights: initialUserBannedRights) }) } else { mode = .chat( @@ -428,7 +445,7 @@ extension ChatControllerImpl { } self.beginDeleteMessagesWithUndo(messageIds: messageIds, type: .forEveryone) }), - TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {}) + TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}) ], actionLayout: .vertical, parseMarkdown: true @@ -490,7 +507,7 @@ extension ChatControllerImpl { } self.beginDeleteMessagesWithUndo(messageIds: messageIds, type: .forEveryone) }), - TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {}) + TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}) ], actionLayout: .vertical, parseMarkdown: true @@ -568,7 +585,7 @@ extension ChatControllerImpl { let dateString = stringForDate(timestamp: giveaway.untilDate, timeZone: .current, strings: strongSelf.presentationData.strings) strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Title, text: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Text(dateString).string, actions: [TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Common_Delete, action: { commit() - }), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { + }), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: { })], parseMarkdown: true), in: .window(.root)) } f(.default) diff --git a/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift b/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift index 6913d4cbb1..71c92156fb 100644 --- a/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift +++ b/submodules/TelegramUI/Sources/ChatControllerForwardMessages.swift @@ -317,14 +317,14 @@ extension ChatControllerImpl { let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: true) commit(transformedMessages) case .schedule: - strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime, repeatPeriod in + strongSelf.presentScheduleTimePicker(completion: { [weak self] timeResult in if let strongSelf = self { - let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: false, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod) + let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: timeResult.silentPosting, scheduleTime: timeResult.time, repeatPeriod: timeResult.repeatPeriod) commit(transformedMessages) } }) case .whenOnline: - let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: false, scheduleTime: scheduleWhenOnlineTimestamp) + let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: strongSelf.presentationInterfaceState.interfaceState.silentPosting, scheduleTime: scheduleWhenOnlineTimestamp) commit(transformedMessages) } } diff --git a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift index cca4be7df8..f3bb5fba6b 100644 --- a/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift +++ b/submodules/TelegramUI/Sources/ChatControllerOpenAttachmentMenu.swift @@ -528,7 +528,9 @@ extension ChatControllerImpl { let contactsController = ContactSelectionControllerImpl(ContactSelectionControllerParams(context: strongSelf.context, style: .glass, updatedPresentationData: strongSelf.updatedPresentationData, title: { $0.Contacts_Title }, displayDeviceContacts: true, multipleSelection: .always, requirePhoneNumbers: true)) contactsController.presentScheduleTimePicker = { [weak self] completion in if let strongSelf = self { - strongSelf.presentScheduleTimePicker(completion: completion) + strongSelf.presentScheduleTimePicker(completion: { result in + completion(result.time, result.repeatPeriod, result.silentPosting) + }) } } contactsController.navigationPresentation = .modal @@ -1057,10 +1059,10 @@ extension ChatControllerImpl { } }, presentSchedulePicker: { [weak self] _, done in if let strongSelf = self { - strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time, repeatPeriod in + strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] result in if let strongSelf = self { - done(time) - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + done(result.time, result.silentPosting) + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp { strongSelf.openScheduledMessages() } } @@ -1114,10 +1116,10 @@ extension ChatControllerImpl { })], actionLayout: .vertical), in: .window(.root)) }, presentSchedulePicker: { [weak self] _, done in if let strongSelf = self { - strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time, repeatPeriod in + strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] result in if let strongSelf = self { - done(time) - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + done(result.time, result.silentPosting) + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp { strongSelf.openScheduledMessages() } } @@ -1367,10 +1369,10 @@ extension ChatControllerImpl { } controller.presentSchedulePicker = { [weak self] media, done in if let strongSelf = self { - strongSelf.presentScheduleTimePicker(style: media ? .media : .default, completion: { [weak self] time, repeatPeriod in + strongSelf.presentScheduleTimePicker(style: media ? .media : .default, completion: { [weak self] result in if let strongSelf = self { - done(time) - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + done(result.time, result.silentPosting) + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp { strongSelf.openScheduledMessages() } } @@ -1495,20 +1497,20 @@ extension ChatControllerImpl { configureLegacyAssetPicker(controller, context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: editingMedia ? nil : { [weak self, weak legacyController] in if let strongSelf = self { - let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: searchBotsConfiguration, mode: .media(attachment: false, completion: { results, selectionState, editingState, silentPosting in + let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: searchBotsConfiguration, mode: .media(attachment: false, completion: { results, selectionState, editingState, silentPosting, scheduleTime in if let legacyController = legacyController { legacyController.dismiss() } legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { result in if let strongSelf = self { - strongSelf.enqueueChatContextResult(results, result, hideVia: true) + strongSelf.enqueueChatContextResult(results, result, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime) } }, enqueueMediaMessages: { signals in if let strongSelf = self { if editingMedia { strongSelf.editMessageMediaWithLegacySignals(signals) } else { - strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting) + strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime) } } }) @@ -1533,10 +1535,10 @@ extension ChatControllerImpl { strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root)) }, presentSchedulePicker: { [weak self] media, done in if let strongSelf = self { - strongSelf.presentScheduleTimePicker(style: media ? .media : .default, completion: { [weak self] time, repeatPeriod in + strongSelf.presentScheduleTimePicker(style: media ? .media : .default, completion: { [weak self] result in if let strongSelf = self { - done(time) - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + done(result.time, result.silentPosting) + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp { strongSelf.openScheduledMessages() } } @@ -1578,18 +1580,18 @@ extension ChatControllerImpl { let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) |> deliverOnMainQueue).startStandalone(next: { [weak self] configuration in if let strongSelf = self { - let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: configuration, mode: .media(attachment: attachment, completion: { [weak self] results, selectionState, editingState, silentPosting in + let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: configuration, mode: .media(attachment: attachment, completion: { [weak self] results, selectionState, editingState, silentPosting, scheduleTime in self?.attachmentController?.dismiss(animated: true, completion: nil) legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak self] result in if let strongSelf = self { - strongSelf.enqueueChatContextResult(results, result, hideVia: true) + strongSelf.enqueueChatContextResult(results, result, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime) } }, enqueueMediaMessages: { [weak self] signals in if let strongSelf = self, !signals.isEmpty { if editingMessage { strongSelf.editMessageMediaWithLegacySignals(signals) } else { - strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting) + strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime) } } }) @@ -1967,10 +1969,10 @@ extension ChatControllerImpl { } }, presentSchedulePicker: { [weak self] _, done in if let strongSelf = self { - strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time, repeatPeriod in + strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] result in if let strongSelf = self { - done(time) - if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { + done(result.time, result.silentPosting) + if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp { strongSelf.openScheduledMessages() } } diff --git a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift index 0944f199b8..66ccd16977 100644 --- a/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift +++ b/submodules/TelegramUI/Sources/ChatInterfaceInputContextPanels.swift @@ -27,6 +27,17 @@ private func inputQueryResultPriority(_ result: ChatPresentationInputQueryResult } } +private func hasBannedInlineContent(chatPresentationInterfaceState: ChatPresentationInterfaceState) -> Bool { + if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel { + let canBypass = canBypassRestrictions(chatPresentationInterfaceState: chatPresentationInterfaceState) + return channel.hasBannedPermission(.banSendInline, ignoreDefault: canBypass) != nil + } else if let group = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramGroup { + return group.hasBannedPermission(.banSendInline) + } else { + return false + } +} + func textInputContextPanel(context: AccountContext, chatPresentationInterfaceState: ChatPresentationInterfaceState, controllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?, currentPanel: ChatInputContextPanelNode?) -> ChatInputContextPanelNode? { guard let controllerInteraction else { return nil @@ -45,15 +56,8 @@ func textInputContextPanel(context: AccountContext, chatPresentationInterfaceSta }).first else { return nil } - - var hasBannedInlineContent = false - if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.hasBannedPermission(.banSendInline) != nil { - hasBannedInlineContent = true - } else if let group = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramGroup, group.hasBannedPermission(.banSendInline) { - hasBannedInlineContent = true - } - - if hasBannedInlineContent { + + if hasBannedInlineContent(chatPresentationInterfaceState: chatPresentationInterfaceState) { switch inputQueryResult { case .stickers, .contextRequestResult: if let currentPanel = currentPanel as? DisabledContextResultsChatInputContextPanelNode { @@ -183,15 +187,8 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa }).first else { return nil } - - var hasBannedInlineContent = false - if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.hasBannedPermission(.banSendInline) != nil { - hasBannedInlineContent = true - } else if let group = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramGroup, group.hasBannedPermission(.banSendInline) { - hasBannedInlineContent = true - } - - if hasBannedInlineContent { + + if hasBannedInlineContent(chatPresentationInterfaceState: chatPresentationInterfaceState) { switch inputQueryResult { case .stickers, .contextRequestResult: if let currentPanel = currentPanel as? DisabledContextResultsChatInputContextPanelNode { @@ -305,4 +302,3 @@ func chatOverlayContextPanelForChatPresentationIntefaceState(_ chatPresentationI return nil } - diff --git a/submodules/TelegramUI/Sources/ContactSelectionController.swift b/submodules/TelegramUI/Sources/ContactSelectionController.swift index 047253a186..531c915b52 100644 --- a/submodules/TelegramUI/Sources/ContactSelectionController.swift +++ b/submodules/TelegramUI/Sources/ContactSelectionController.swift @@ -70,7 +70,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController private let isPeerEnabled: (ContactListPeer) -> Bool var dismissed: (() -> Void)? - var presentScheduleTimePicker: (@escaping (Int32, Int32?) -> Void) -> Void = { _ in } + var presentScheduleTimePicker: (@escaping (Int32, Int32?, Bool) -> Void) -> Void = { _ in } private let createActionDisposable = MetaDisposable() private let confirmationDisposable = MetaDisposable() @@ -646,8 +646,8 @@ final class ContactsPickerContext: AttachmentMediaPickerContext { } func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { - self.controller?.presentScheduleTimePicker ({ time, repeatPeriod in - self.controller?.contactsNode.requestMultipleAction?(false, time, parameters) + self.controller?.presentScheduleTimePicker ({ time, repeatPeriod, silentPosting in + self.controller?.contactsNode.requestMultipleAction?(silentPosting, time, parameters) }) } diff --git a/submodules/TelegramUI/Sources/CreateChannelController.swift b/submodules/TelegramUI/Sources/CreateChannelController.swift index 1aac1f9485..eddc7f5297 100644 --- a/submodules/TelegramUI/Sources/CreateChannelController.swift +++ b/submodules/TelegramUI/Sources/CreateChannelController.swift @@ -19,8 +19,12 @@ import PeerInfoUI import MapResourceToAvatarSizes import LegacyMediaPickerUI import TextFormat +import MediaEditor +import MediaEditorScreen +import CameraScreen import AvatarEditorScreen import OldChannelsController +import Photos import AVFoundation private struct CreateChannelArguments { @@ -363,10 +367,29 @@ public func createChannelController(context: AccountContext, mode: CreateChannel let actionsDisposable = DisposableSet() - let currentAvatarMixin = Atomic(value: nil) - - let uploadedAvatar = Promise() - var uploadedVideoAvatar: (Promise, Double?)? = nil + var avatarPickerHolder: Any? + var pendingAvatar: CreatePendingPeerAvatar? + let applyPendingAvatar: (CreatePendingPeerAvatar) -> Void = { avatar in + pendingAvatar = avatar + updateState { current in + var current = current + current.avatar = avatar.updatingAvatar + return current + } + } + let updatePendingAvatarIfCurrent: (CreatePendingPeerAvatar) -> Void = { avatar in + if pendingAvatar?.previewRepresentation.resource.id == avatar.previewRepresentation.resource.id { + applyPendingAvatar(avatar) + } + } + let clearPendingAvatar: () -> Void = { + pendingAvatar = nil + updateState { current in + var current = current + current.avatar = nil + return current + } + } let checkAddressNameDisposable = MetaDisposable() actionsDisposable.add(checkAddressNameDisposable) @@ -434,11 +457,8 @@ public func createChannelController(context: AccountContext, mode: CreateChannel } } }).start(next: { peerId in - let updatingAvatar = stateValue.with { - return $0.avatar - } - if let _ = updatingAvatar { - let _ = context.engine.peers.updatePeerPhoto(peerId: peerId, photo: uploadedAvatar.get(), video: uploadedVideoAvatar?.0.get(), videoStartTimestamp: uploadedVideoAvatar?.1, mapResourceToAvatarSizes: { resource, representations in + if let pendingAvatar { + let _ = context.engine.peers.updatePeerPhoto(peerId: peerId, photo: pendingAvatar.uploadedPhoto, video: pendingAvatar.uploadedVideo, videoStartTimestamp: pendingAvatar.videoStartTimestamp, markup: pendingAvatar.markup, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(engine: context.engine, resource: resource, representations: representations) }).start() } @@ -474,216 +494,127 @@ public func createChannelController(context: AccountContext, mode: CreateChannel }, changeProfilePhoto: { endEditingImpl?() - let title = stateValue.with { state -> String in - return state.editingName.composedTitle - } + let keyboardInputData = Promise() + keyboardInputData.set(AvatarEditorScreen.inputData(context: context, isGroup: true)) - let _ = (context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), - TelegramEngine.EngineData.Item.Configuration.SearchBots() - ) - |> deliverOnMainQueue).start(next: { peer, searchBotsConfiguration in - let presentationData = context.sharedContext.currentPresentationData.with { $0 } + var dismissPickerImpl: (() -> Void)? + let (mainController, pickerHolder) = context.sharedContext.makeAvatarMediaPickerScreen(context: context, getSourceRect: { return nil }, canDelete: pendingAvatar != nil, performDelete: { + clearPendingAvatar() + }, completion: { result, transitionView, transitionRect, transitionImage, fromCamera, _, cancelled in + avatarPickerHolder = nil - let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) - legacyController.statusBar.statusBarStyle = .Ignore - - let emptyController = LegacyEmptyController(context: legacyController.context)! - let navigationController = makeLegacyNavigationController(rootController: emptyController) - navigationController.setNavigationBarHidden(true, animated: false) - navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) - - legacyController.bind(controller: navigationController) - - endEditingImpl?() - presentControllerImpl?(legacyController, nil) - - let completedChannelPhotoImpl: (UIImage) -> Void = { image in - if let data = image.jpegData(compressionQuality: 0.6) { - let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) - uploadedAvatar.set(context.engine.peers.uploadedPeerPhoto(resource: EngineMediaResource(resource))) - uploadedVideoAvatar = nil - updateState { current in - var current = current - current.avatar = .image(representation, false) - return current - } + let applyPhoto: (UIImage) -> Void = { image in + if let avatar = CreatePeerAvatarSetup.photo(context: context, image: image) { + applyPendingAvatar(avatar) + } + } + let applyVideo: (UIImage, MediaEditorScreenImpl.MediaResult.VideoResult?, MediaEditorValues?, UploadPeerPhotoMarkup?) -> Void = { image, video, values, markup in + if let avatar = CreatePeerAvatarSetup.video(context: context, image: image, video: video, values: values, markup: markup, didCompleteLoadingPreview: { avatar in + updatePendingAvatarIfCurrent(avatar) + }) { + applyPendingAvatar(avatar) } } - let completedChannelVideoImpl: (UIImage, Any?, TGVideoEditAdjustments?) -> Void = { image, asset, adjustments in - if let data = image.jpegData(compressionQuality: 0.6) { - let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.engine.resources.storeResourceData(id: EngineMediaResource.Id(photoResource.id), data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) - updateState { state in - var state = state - state.avatar = .image(representation, true) - return state + let subject: Signal + if let asset = result as? PHAsset { + subject = .single(.asset(asset)) + } else if let image = result as? UIImage { + subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight, fromCamera: false)) + } else if let result = result as? Signal { + subject = result + |> map { value -> MediaEditorScreenImpl.Subject? in + switch value { + case .pendingImage: + return nil + case let .image(image): + return .image(image: image.image, dimensions: PixelDimensions(image.image.size), additionalImage: nil, additionalImagePosition: .topLeft, fromCamera: false) + case let .video(video): + return .video(videoPath: video.videoPath, thumbnail: video.coverImage, mirror: video.mirror, additionalVideoPath: nil, additionalThumbnail: nil, dimensions: video.dimensions, duration: video.duration, videoPositionChanges: [], additionalVideoPosition: .topLeft, fromCamera: false) + default: + return nil } - - var videoStartTimestamp: Double? = nil - if let adjustments = adjustments, adjustments.videoStartValue > 0.0 { - videoStartTimestamp = adjustments.videoStartValue - adjustments.trimStartValue - } - - let signal = Signal { subscriber in - let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in - if let paintingData = adjustments.paintingData, paintingData.hasAnimation { - return LegacyPaintEntityRenderer(postbox: context.account.postbox, adjustments: adjustments) - } else { - return nil - } - } - - let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4") - let uploadInterface = LegacyLiveUploadInterface(context: context) - let signal: SSignal - if let url = asset as? URL, url.absoluteString.hasSuffix(".jpg"), let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer { - let durationSignal: SSignal = SSignal(generator: { subscriber in - let disposable = (entityRenderer.duration()).start(next: { duration in - subscriber.putNext(duration) - subscriber.putCompletion() - }) - - return SBlockDisposable(block: { - disposable.dispose() - }) - }) - signal = durationSignal.map(toSignal: { duration -> SSignal in - if let duration = duration as? Double { - return TGMediaVideoConverter.renderUIImage(image, duration: duration, adjustments: adjustments, path: tempFile.path, watcher: nil, entityRenderer: entityRenderer)! - } else { - return SSignal.single(nil) - } - }) - - } else if let asset = asset as? AVAsset { - signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, path: tempFile.path, watcher: uploadInterface, entityRenderer: entityRenderer)! - } else { - signal = SSignal.complete() - } - - let signalDisposable = signal.start(next: { next in - if let result = next as? TGMediaVideoConversionResult { - if let image = result.coverImage, let data = image.jpegData(compressionQuality: 0.7) { - context.engine.resources.storeResourceData(id: EngineMediaResource.Id(photoResource.id), data: data) - } - - if let timestamp = videoStartTimestamp { - videoStartTimestamp = max(0.0, min(timestamp, result.duration - 0.05)) - } - - var value = stat() - if stat(result.fileURL.path, &value) == 0 { - if let data = try? Data(contentsOf: result.fileURL) { - let resource: TelegramMediaResource - if let liveUploadData = result.liveUploadData as? LegacyLiveUploadInterfaceResult { - resource = LocalFileMediaResource(fileId: liveUploadData.id) - } else { - resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - } - context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) - subscriber.putNext(resource) - - EngineTempBox.shared.dispose(tempFile) - } - } - subscriber.putCompletion() - } - }, error: { _ in - }, completed: nil) - - let disposable = ActionDisposable { - signalDisposable?.dispose() - } - - return ActionDisposable { - disposable.dispose() - } - } - - uploadedAvatar.set(context.engine.peers.uploadedPeerPhoto(resource: EngineMediaResource(photoResource))) - - let promise = Promise() - promise.set(signal - |> `catch` { _ -> Signal in - return .single(nil) - } - |> mapToSignal { resource -> Signal in - if let resource = resource { - return context.engine.peers.uploadedPeerVideo(resource: EngineMediaResource(resource)) |> map(Optional.init) - } else { - return .single(nil) - } - } |> afterNext { next in - if let next = next, next.isCompleted { - updateState { state in - var state = state - state.avatar = .image(representation, false) - return state - } - } - }) - uploadedVideoAvatar = (promise, videoStartTimestamp) } + } else { + let controller = AvatarEditorScreen(context: context, inputData: keyboardInputData.get(), peerType: .channel, markup: nil) + controller.imageCompletion = { image, commit in + applyPhoto(image) + commit() + } + controller.videoCompletion = { image, _, _, markup, commit in + applyVideo(image, nil, nil, markup) + commit() + } + pushControllerImpl?(controller) + return } - let keyboardInputData = Promise() - keyboardInputData.set(AvatarEditorScreen.inputData(context: context, isGroup: true)) - - let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, hasViewButton: false, personalPhoto: false, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false, title: nil, isSuggesting: false)! - mixin.stickersContext = LegacyPaintStickersContext(context: context) - let _ = currentAvatarMixin.swap(mixin) - mixin.requestSearchController = { assetsController in - let controller = WebSearchController(context: context, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: title, completion: { result in - assetsController?.dismiss() - completedChannelPhotoImpl(result) - })) - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - } -// mixin.requestAvatarEditor = { imageCompletion, videoCompletion in -// guard let imageCompletion, let videoCompletion else { -// return -// } -// let controller = AvatarEditorScreen(context: context, inputData: keyboardInputData.get(), peerType: .channel, markup: nil) -// controller.imageCompletion = imageCompletion -// controller.videoCompletion = videoCompletion -// pushControllerImpl?(controller) -// } - mixin.didFinishWithImage = { image in - if let image = image { - completedChannelPhotoImpl(image) - } - } - mixin.didFinishWithVideo = { image, asset, adjustments in - if let image = image, let asset = asset { - completedChannelVideoImpl(image, asset, adjustments) - } - } - if stateValue.with({ $0.avatar }) != nil { - mixin.didFinishWithDelete = { - updateState { current in - var current = current - current.avatar = nil - return current + let editorController = MediaEditorScreenImpl( + context: context, + mode: .avatarEditor, + subject: subject, + transitionIn: fromCamera ? .camera : transitionView.flatMap({ .gallery( + MediaEditorScreenImpl.TransitionIn.GalleryTransitionIn( + sourceView: $0, + sourceRect: transitionRect, + sourceImage: transitionImage + ) + ) }), + transitionOut: { finished, _ in + if !finished, let transitionView { + return MediaEditorScreenImpl.TransitionOut( + destinationView: transitionView, + destinationRect: transitionView.bounds, + destinationCornerRadius: 0.0 + ) } - uploadedAvatar.set(.never()) - } - } - mixin.didDismiss = { [weak legacyController] in - let _ = currentAvatarMixin.swap(nil) - legacyController?.dismiss() - } - let menuController = mixin.present() - if let menuController = menuController { - menuController.customRemoveFromParentViewController = { [weak legacyController] in - legacyController?.dismiss() - } + return nil + }, + completion: { results, commit in + guard let result = results.first else { + return + } + switch result.media { + case let .image(image, _): + applyPhoto(image) + commit({}) + case let .video(video, coverImage, values, _, _): + if let coverImage { + applyVideo(coverImage, video, values, nil) + } + commit({}) + default: + break + } + dismissPickerImpl?() + } as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void + ) + editorController.cancelled = { _ in + cancelled() } + pushControllerImpl?(editorController) + }, dismissed: { + avatarPickerHolder = nil }) + avatarPickerHolder = pickerHolder + if let mainController { + dismissPickerImpl = { [weak mainController] in + if let mainController, let navigationController = mainController.navigationController { + var viewControllers = navigationController.viewControllers + viewControllers = viewControllers.filter { controller in + return !(controller is CameraScreen) && controller !== mainController + } + navigationController.setViewControllers(viewControllers, animated: false) + } + } + if mainController is ActionSheetController { + presentControllerImpl?(mainController, nil) + } else { + mainController.navigationPresentation = .flatModal + mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + pushControllerImpl?(mainController) + } + } }, focusOnDescription: { focusOnDescriptionImpl?() }, updatePublicLinkText: { text in @@ -741,6 +672,8 @@ public func createChannelController(context: AccountContext, mode: CreateChannel return (controllerState, (listState, arguments)) } |> afterDisposed { actionsDisposable.dispose() + + let _ = avatarPickerHolder } let controller = ItemListController(context: context, state: signal) diff --git a/submodules/TelegramUI/Sources/CreateGroupController.swift b/submodules/TelegramUI/Sources/CreateGroupController.swift index ce7fbe0ccb..f0ce6b6cc2 100644 --- a/submodules/TelegramUI/Sources/CreateGroupController.swift +++ b/submodules/TelegramUI/Sources/CreateGroupController.swift @@ -34,6 +34,10 @@ import TextFormat import AvatarEditorScreen import SendInviteLinkScreen import OldChannelsController +import MediaEditor +import MediaEditorScreen +import CameraScreen +import Photos import AVFoundation private struct CreateGroupArguments { @@ -563,10 +567,29 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] let checkAddressNameDisposable = MetaDisposable() actionsDisposable.add(checkAddressNameDisposable) - let currentAvatarMixin = Atomic(value: nil) - - let uploadedAvatar = Promise() - var uploadedVideoAvatar: (Promise, Double?)? = nil + var avatarPickerHolder: Any? + var pendingAvatar: CreatePendingPeerAvatar? + let applyPendingAvatar: (CreatePendingPeerAvatar) -> Void = { avatar in + pendingAvatar = avatar + updateState { current in + var current = current + current.avatar = avatar.updatingAvatar + return current + } + } + let updatePendingAvatarIfCurrent: (CreatePendingPeerAvatar) -> Void = { avatar in + if pendingAvatar?.previewRepresentation.resource.id == avatar.previewRepresentation.resource.id { + applyPendingAvatar(avatar) + } + } + let clearPendingAvatar: () -> Void = { + pendingAvatar = nil + updateState { current in + var current = current + current.avatar = nil + return current + } + } if initialTitle == nil && peerIds.count > 0 && peerIds.count < 5 { let _ = (context.engine.data.get( @@ -793,18 +816,14 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] let _ = createSignal let _ = replaceControllerImpl let _ = dismissImpl - let _ = uploadedVideoAvatar actionsDisposable.add((createSignal |> mapToSignal { result -> Signal in guard let result = result else { return .single(nil) } - let updatingAvatar = stateValue.with { - return $0.avatar - } - if let _ = updatingAvatar { - return context.engine.peers.updatePeerPhoto(peerId: result.peerId, photo: uploadedAvatar.get(), video: uploadedVideoAvatar?.0.get(), videoStartTimestamp: uploadedVideoAvatar?.1, mapResourceToAvatarSizes: { resource, representations in + if let pendingAvatar { + return context.engine.peers.updatePeerPhoto(peerId: result.peerId, photo: pendingAvatar.uploadedPhoto, video: pendingAvatar.uploadedVideo, videoStartTimestamp: pendingAvatar.videoStartTimestamp, markup: pendingAvatar.markup, mapResourceToAvatarSizes: { resource, representations in return mapResourceToAvatarSizes(engine: context.engine, resource: resource, representations: representations) }) |> ignoreValues @@ -893,216 +912,134 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] }, changeProfilePhoto: { endEditingImpl?() - let title = stateValue.with { state -> String in - return state.editingName.composedTitle + let peerType: AvatarEditorScreen.PeerType + if case let .requestPeer(group) = mode, group.isForum == true { + peerType = .forum + } else { + peerType = .group } - let _ = (context.engine.data.get( - TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), - TelegramEngine.EngineData.Item.Configuration.SearchBots() - ) - |> deliverOnMainQueue).start(next: { peer, searchBotsConfiguration in - let presentationData = context.sharedContext.currentPresentationData.with { $0 } + let keyboardInputData = Promise() + keyboardInputData.set(AvatarEditorScreen.inputData(context: context, isGroup: true)) + + var dismissPickerImpl: (() -> Void)? + let (mainController, pickerHolder) = context.sharedContext.makeAvatarMediaPickerScreen(context: context, getSourceRect: { return nil }, canDelete: pendingAvatar != nil, performDelete: { + clearPendingAvatar() + }, completion: { result, transitionView, transitionRect, transitionImage, fromCamera, _, cancelled in + avatarPickerHolder = nil - let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) - legacyController.statusBar.statusBarStyle = .Ignore - - let emptyController = LegacyEmptyController(context: legacyController.context)! - let navigationController = makeLegacyNavigationController(rootController: emptyController) - navigationController.setNavigationBarHidden(true, animated: false) - navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) - - legacyController.bind(controller: navigationController) - - endEditingImpl?() - presentControllerImpl?(legacyController, nil) - - let completedGroupPhotoImpl: (UIImage) -> Void = { image in - if let data = image.jpegData(compressionQuality: 0.6) { - let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) - uploadedAvatar.set(context.engine.peers.uploadedPeerPhoto(resource: EngineMediaResource(resource))) - uploadedVideoAvatar = nil - updateState { current in - var current = current - current.avatar = .image(representation, false) - return current - } + let applyPhoto: (UIImage) -> Void = { image in + if let avatar = CreatePeerAvatarSetup.photo(context: context, image: image) { + applyPendingAvatar(avatar) + } + } + let applyVideo: (UIImage, MediaEditorScreenImpl.MediaResult.VideoResult?, MediaEditorValues?, UploadPeerPhotoMarkup?) -> Void = { image, video, values, markup in + if let avatar = CreatePeerAvatarSetup.video(context: context, image: image, video: video, values: values, markup: markup, didCompleteLoadingPreview: { avatar in + updatePendingAvatarIfCurrent(avatar) + }) { + applyPendingAvatar(avatar) } } - let completedGroupVideoImpl: (UIImage, Any?, TGVideoEditAdjustments?) -> Void = { image, asset, adjustments in - if let data = image.jpegData(compressionQuality: 0.6) { - let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - context.engine.resources.storeResourceData(id: EngineMediaResource.Id(photoResource.id), data: data) - let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) - updateState { state in - var state = state - state.avatar = .image(representation, true) - return state + let subject: Signal + if let asset = result as? PHAsset { + subject = .single(.asset(asset)) + } else if let image = result as? UIImage { + subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight, fromCamera: false)) + } else if let result = result as? Signal { + subject = result + |> map { value -> MediaEditorScreenImpl.Subject? in + switch value { + case .pendingImage: + return nil + case let .image(image): + return .image(image: image.image, dimensions: PixelDimensions(image.image.size), additionalImage: nil, additionalImagePosition: .topLeft, fromCamera: false) + case let .video(video): + return .video(videoPath: video.videoPath, thumbnail: video.coverImage, mirror: video.mirror, additionalVideoPath: nil, additionalThumbnail: nil, dimensions: video.dimensions, duration: video.duration, videoPositionChanges: [], additionalVideoPosition: .topLeft, fromCamera: false) + default: + return nil } - - var videoStartTimestamp: Double? = nil - if let adjustments = adjustments, adjustments.videoStartValue > 0.0 { - videoStartTimestamp = adjustments.videoStartValue - adjustments.trimStartValue - } - - let signal = Signal { subscriber in - let entityRenderer: LegacyPaintEntityRenderer? = adjustments.flatMap { adjustments in - if let paintingData = adjustments.paintingData, paintingData.hasAnimation { - return LegacyPaintEntityRenderer(postbox: context.account.postbox, adjustments: adjustments) - } else { - return nil - } - } - - let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4") - let uploadInterface = LegacyLiveUploadInterface(context: context) - let signal: SSignal - if let url = asset as? URL, url.absoluteString.hasSuffix(".jpg"), let data = try? Data(contentsOf: url, options: [.mappedRead]), let image = UIImage(data: data), let entityRenderer = entityRenderer { - let durationSignal: SSignal = SSignal(generator: { subscriber in - let disposable = (entityRenderer.duration()).start(next: { duration in - subscriber.putNext(duration) - subscriber.putCompletion() - }) - - return SBlockDisposable(block: { - disposable.dispose() - }) - }) - signal = durationSignal.map(toSignal: { duration -> SSignal in - if let duration = duration as? Double { - return TGMediaVideoConverter.renderUIImage(image, duration: duration, adjustments: adjustments, path: tempFile.path, watcher: nil, entityRenderer: entityRenderer)! - } else { - return SSignal.single(nil) - } - }) - - } else if let asset = asset as? AVAsset { - signal = TGMediaVideoConverter.convert(asset, adjustments: adjustments, path: tempFile.path, watcher: uploadInterface, entityRenderer: entityRenderer)! - } else { - signal = SSignal.complete() - } - - let signalDisposable = signal.start(next: { next in - if let result = next as? TGMediaVideoConversionResult { - if let image = result.coverImage, let data = image.jpegData(compressionQuality: 0.7) { - context.engine.resources.storeResourceData(id: EngineMediaResource.Id(photoResource.id), data: data) - } - - if let timestamp = videoStartTimestamp { - videoStartTimestamp = max(0.0, min(timestamp, result.duration - 0.05)) - } - - var value = stat() - if stat(result.fileURL.path, &value) == 0 { - if let data = try? Data(contentsOf: result.fileURL) { - let resource: TelegramMediaResource - if let liveUploadData = result.liveUploadData as? LegacyLiveUploadInterfaceResult { - resource = LocalFileMediaResource(fileId: liveUploadData.id) - } else { - resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) - } - context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data, synchronous: true) - subscriber.putNext(resource) - - EngineTempBox.shared.dispose(tempFile) - } - } - subscriber.putCompletion() - } - }, error: { _ in - }, completed: nil) - - let disposable = ActionDisposable { - signalDisposable?.dispose() - } - - return ActionDisposable { - disposable.dispose() - } - } - - uploadedAvatar.set(context.engine.peers.uploadedPeerPhoto(resource: EngineMediaResource(photoResource))) - - let promise = Promise() - promise.set(signal - |> `catch` { _ -> Signal in - return .single(nil) - } - |> mapToSignal { resource -> Signal in - if let resource = resource { - return context.engine.peers.uploadedPeerVideo(resource: EngineMediaResource(resource)) |> map(Optional.init) - } else { - return .single(nil) - } - } |> afterNext { next in - if let next = next, next.isCompleted { - updateState { state in - var state = state - state.avatar = .image(representation, false) - return state - } - } - }) - uploadedVideoAvatar = (promise, videoStartTimestamp) } + } else { + let controller = AvatarEditorScreen(context: context, inputData: keyboardInputData.get(), peerType: peerType, markup: nil) + controller.imageCompletion = { image, commit in + applyPhoto(image) + commit() + } + controller.videoCompletion = { image, _, _, markup, commit in + applyVideo(image, nil, nil, markup) + commit() + } + pushImpl?(controller) + return } - let keyboardInputData = Promise() - keyboardInputData.set(AvatarEditorScreen.inputData(context: context, isGroup: true)) - - let mixin = TGMediaAvatarMenuMixin(context: legacyController.context, parentController: emptyController, hasSearchButton: true, hasDeleteButton: stateValue.with({ $0.avatar }) != nil, hasViewButton: false, personalPhoto: false, isVideo: false, saveEditedPhotos: false, saveCapturedMedia: false, signup: false, forum: false, title: nil, isSuggesting: false)! - mixin.stickersContext = LegacyPaintStickersContext(context: context) - let _ = currentAvatarMixin.swap(mixin) - mixin.requestSearchController = { assetsController in - let controller = WebSearchController(context: context, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: title, completion: { result in - assetsController?.dismiss() - completedGroupPhotoImpl(result) - })) - presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) - } -// mixin.requestAvatarEditor = { imageCompletion, videoCompletion in -// guard let imageCompletion, let videoCompletion else { -// return -// } -// let controller = AvatarEditorScreen(context: context, inputData: keyboardInputData.get(), peerType: .group, markup: nil) -// controller.imageCompletion = imageCompletion -// controller.videoCompletion = videoCompletion -// pushImpl?(controller) -// } - mixin.didFinishWithImage = { image in - if let image = image { - completedGroupPhotoImpl(image) - } - } - mixin.didFinishWithVideo = { image, asset, adjustments in - if let image = image, let asset = asset { - completedGroupVideoImpl(image, asset, adjustments) - } - } - if stateValue.with({ $0.avatar }) != nil { - mixin.didFinishWithDelete = { - updateState { current in - var current = current - current.avatar = nil - return current + let editorController = MediaEditorScreenImpl( + context: context, + mode: .avatarEditor, + subject: subject, + transitionIn: fromCamera ? .camera : transitionView.flatMap({ .gallery( + MediaEditorScreenImpl.TransitionIn.GalleryTransitionIn( + sourceView: $0, + sourceRect: transitionRect, + sourceImage: transitionImage + ) + ) }), + transitionOut: { finished, _ in + if !finished, let transitionView { + return MediaEditorScreenImpl.TransitionOut( + destinationView: transitionView, + destinationRect: transitionView.bounds, + destinationCornerRadius: 0.0 + ) } - uploadedAvatar.set(.never()) - } - } - mixin.didDismiss = { [weak legacyController] in - let _ = currentAvatarMixin.swap(nil) - legacyController?.dismiss() - } - let menuController = mixin.present() - if let menuController = menuController { - menuController.customRemoveFromParentViewController = { [weak legacyController] in - legacyController?.dismiss() - } + return nil + }, + completion: { results, commit in + guard let result = results.first else { + return + } + switch result.media { + case let .image(image, _): + applyPhoto(image) + commit({}) + case let .video(video, coverImage, values, _, _): + if let coverImage { + applyVideo(coverImage, video, values, nil) + } + commit({}) + default: + break + } + dismissPickerImpl?() + } as ([MediaEditorScreenImpl.Result], @escaping (@escaping () -> Void) -> Void) -> Void + ) + editorController.cancelled = { _ in + cancelled() } + pushImpl?(editorController) + }, dismissed: { + avatarPickerHolder = nil }) + avatarPickerHolder = pickerHolder + if let mainController { + dismissPickerImpl = { [weak mainController] in + if let mainController, let navigationController = mainController.navigationController { + var viewControllers = navigationController.viewControllers + viewControllers = viewControllers.filter { controller in + return !(controller is CameraScreen) && controller !== mainController + } + navigationController.setViewControllers(viewControllers, animated: false) + } + } + if mainController is ActionSheetController { + presentControllerImpl?(mainController, nil) + } else { + mainController.navigationPresentation = .flatModal + mainController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) + pushImpl?(mainController) + } + } }, changeLocation: { endEditingImpl?() @@ -1313,6 +1250,8 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId] } |> afterDisposed { actionsDisposable.dispose() + + let _ = avatarPickerHolder } let controller = ItemListController(context: context, state: signal) diff --git a/submodules/TelegramUI/Sources/CreatePeerAvatarSetup.swift b/submodules/TelegramUI/Sources/CreatePeerAvatarSetup.swift new file mode 100644 index 0000000000..603e188909 --- /dev/null +++ b/submodules/TelegramUI/Sources/CreatePeerAvatarSetup.swift @@ -0,0 +1,216 @@ +import Foundation +import UIKit +import SwiftSignalKit +import TelegramCore +import AccountContext +import MediaEditor +import MediaEditorScreen +import ItemListAvatarAndNameInfoItem +import Photos +import AVFoundation + +struct CreatePendingPeerAvatar { + let previewRepresentation: TelegramMediaImageRepresentation + let isLoadingPreview: Bool + let uploadedPhoto: Signal + let uploadedVideo: Signal? + let videoStartTimestamp: Double? + let markup: UploadPeerPhotoMarkup? + + var updatingAvatar: ItemListAvatarAndNameInfoItemUpdatingAvatar { + return .image(self.previewRepresentation, self.isLoadingPreview) + } +} + +enum CreatePeerAvatarSetup { + private static func makePhotoRepresentation(context: AccountContext, image: UIImage) -> (LocalFileMediaResource, TelegramMediaImageRepresentation)? { + guard let data = image.jpegData(compressionQuality: 0.6) else { + return nil + } + + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + context.engine.resources.storeResourceData(id: EngineMediaResource.Id(resource.id), data: data) + let representation = TelegramMediaImageRepresentation( + dimensions: PixelDimensions(width: 640, height: 640), + resource: resource, + progressiveSizes: [], + immediateThumbnailData: nil, + hasVideo: false, + isPersonal: false + ) + return (resource, representation) + } + + static func photo(context: AccountContext, image: UIImage) -> CreatePendingPeerAvatar? { + guard let (resource, representation) = self.makePhotoRepresentation(context: context, image: image) else { + return nil + } + + return CreatePendingPeerAvatar( + previewRepresentation: representation, + isLoadingPreview: false, + uploadedPhoto: context.engine.peers.uploadedPeerPhoto(resource: EngineMediaResource(resource)), + uploadedVideo: nil, + videoStartTimestamp: nil, + markup: nil + ) + } + + static func video( + context: AccountContext, + image: UIImage, + video: MediaEditorScreenImpl.MediaResult.VideoResult?, + values: MediaEditorValues?, + markup: UploadPeerPhotoMarkup?, + didCompleteLoadingPreview: @escaping (CreatePendingPeerAvatar) -> Void = { _ in } + ) -> CreatePendingPeerAvatar? { + var shouldUploadVideo = true + if markup != nil { + if let data = context.currentAppConfiguration.with({ $0 }).data, let uploadVideoValue = data["upload_markup_video"] as? Bool, uploadVideoValue { + shouldUploadVideo = true + } else { + shouldUploadVideo = false + } + } + + guard let (photoResource, representation) = self.makePhotoRepresentation(context: context, image: image) else { + return nil + } + + let uploadedPhoto = context.engine.peers.uploadedPeerPhoto(resource: EngineMediaResource(photoResource)) + + var videoStartTimestamp: Double? = nil + if let values, let coverImageTimestamp = values.coverImageTimestamp, coverImageTimestamp > 0.0 { + videoStartTimestamp = coverImageTimestamp - (values.videoTrimRange?.lowerBound ?? 0.0) + } + + let hasVideoUpload = shouldUploadVideo && video != nil && values != nil + guard hasVideoUpload, let video, let values else { + return CreatePendingPeerAvatar( + previewRepresentation: representation, + isLoadingPreview: false, + uploadedPhoto: uploadedPhoto, + uploadedVideo: nil, + videoStartTimestamp: videoStartTimestamp, + markup: markup + ) + } + + let account = context.account + let videoResource: Signal + + var exportSubject: Signal<(MediaEditorVideoExport.Subject, Double), NoError>? + switch video { + case let .imageFile(path): + if let image = UIImage(contentsOfFile: path) { + exportSubject = .single((.image(image: image), 3.0)) + } + case let .videoFile(path): + let asset = AVURLAsset(url: NSURL(fileURLWithPath: path) as URL) + exportSubject = .single((.video(asset: asset, isStory: false), asset.duration.seconds)) + case let .asset(localIdentifier): + exportSubject = Signal { subscriber in + let fetchResult = PHAsset.fetchAssets(withLocalIdentifiers: [localIdentifier], options: nil) + if fetchResult.count != 0 { + let asset = fetchResult.object(at: 0) + if asset.mediaType == .video { + PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in + if let avAsset { + subscriber.putNext((.video(asset: avAsset, isStory: true), avAsset.duration.seconds)) + subscriber.putCompletion() + } + } + } else { + let options = PHImageRequestOptions() + options.deliveryMode = .highQualityFormat + PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in + if let image { + subscriber.putNext((.image(image: image), 3.0)) + subscriber.putCompletion() + } + } + } + } + return EmptyDisposable + } + } + + guard let exportSubject else { + return CreatePendingPeerAvatar( + previewRepresentation: representation, + isLoadingPreview: false, + uploadedPhoto: uploadedPhoto, + uploadedVideo: nil, + videoStartTimestamp: videoStartTimestamp, + markup: markup + ) + } + + videoResource = exportSubject + |> castError(UploadPeerPhotoError.self) + |> mapToSignal { exportSubject, duration in + return Signal { subscriber in + let configuration = recommendedVideoExportConfiguration(values: values, duration: duration, forceFullHd: true, frameRate: 60.0, isAvatar: true) + let tempFile = EngineTempBox.shared.tempFile(fileName: "video.mp4") + let videoExport = MediaEditorVideoExport(postbox: context.account.postbox, subject: exportSubject, configuration: configuration, outputPath: tempFile.path, textScale: 2.0) + let _ = (videoExport.status + |> deliverOnMainQueue).startStandalone(next: { status in + switch status { + case .completed: + if let data = try? Data(contentsOf: URL(fileURLWithPath: tempFile.path), options: .mappedIfSafe) { + let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) + account.postbox.mediaBox.storeResourceData(resource.id, data: data, synchronous: true) + subscriber.putNext(resource) + subscriber.putCompletion() + } + EngineTempBox.shared.dispose(tempFile) + case .progress: + break + default: + break + } + }) + + return EmptyDisposable + } + } + + var completedAvatar: CreatePendingPeerAvatar? + let uploadedVideo = (videoResource + |> `catch` { _ -> Signal in + return .single(nil) + } + |> mapToSignal { resource -> Signal in + if let resource { + return context.engine.peers.uploadedPeerVideo(resource: EngineMediaResource(resource)) + |> map(Optional.init) + } else { + return .single(nil) + } + } + |> afterNext { next in + if let next, next.isCompleted, let completedAvatar { + didCompleteLoadingPreview(completedAvatar) + } + }) + + let pendingAvatar = CreatePendingPeerAvatar( + previewRepresentation: representation, + isLoadingPreview: true, + uploadedPhoto: uploadedPhoto, + uploadedVideo: uploadedVideo, + videoStartTimestamp: videoStartTimestamp, + markup: markup + ) + completedAvatar = CreatePendingPeerAvatar( + previewRepresentation: representation, + isLoadingPreview: false, + uploadedPhoto: uploadedPhoto, + uploadedVideo: uploadedVideo, + videoStartTimestamp: videoStartTimestamp, + markup: markup + ) + + return pendingAvatar + } +} diff --git a/submodules/TelegramUI/Sources/DisabledContextResultsChatInputContextPanelNode.swift b/submodules/TelegramUI/Sources/DisabledContextResultsChatInputContextPanelNode.swift index 69025bf1fe..f86bc5defd 100644 --- a/submodules/TelegramUI/Sources/DisabledContextResultsChatInputContextPanelNode.swift +++ b/submodules/TelegramUI/Sources/DisabledContextResultsChatInputContextPanelNode.swift @@ -43,7 +43,8 @@ final class DisabledContextResultsChatInputContextPanelNode: ChatInputContextPan self.containerNode.backgroundColor = interfaceState.theme.list.plainBackgroundColor self.separatorNode.backgroundColor = interfaceState.theme.list.itemPlainSeparatorColor - guard let (untilDate, personal) = (interfaceState.renderedPeer?.peer as? TelegramChannel)?.hasBannedPermission(.banSendInline) else { + let canBypass = canBypassRestrictions(chatPresentationInterfaceState: interfaceState) + guard let (untilDate, personal) = (interfaceState.renderedPeer?.peer as? TelegramChannel)?.hasBannedPermission(.banSendInline, ignoreDefault: canBypass) else { return } let banDescription: String diff --git a/submodules/WebSearchUI/Sources/WebSearchController.swift b/submodules/WebSearchUI/Sources/WebSearchController.swift index 9e960e9a63..32a0fa1aef 100644 --- a/submodules/WebSearchUI/Sources/WebSearchController.swift +++ b/submodules/WebSearchUI/Sources/WebSearchController.swift @@ -16,7 +16,7 @@ public enum WebSearchMode { } public enum WebSearchControllerMode { - case media(attachment: Bool, completion: (ChatContextResultCollection, TGMediaSelectionContext, TGMediaEditingContext, Bool) -> Void) + case media(attachment: Bool, completion: (ChatContextResultCollection, TGMediaSelectionContext, TGMediaEditingContext, Bool, Int32?) -> Void) case editor(completion: (UIImage) -> Void) case avatar(initialQuery: String?, completion: (UIImage) -> Void) @@ -115,7 +115,7 @@ public final class WebSearchController: ViewController { } } - public var presentSchedulePicker: (Bool, @escaping (Int32) -> Void) -> Void = { _, _ in } + public var presentSchedulePicker: (Bool, @escaping (Int32, Bool) -> Void) -> Void = { _, _ in } public var dismissed: () -> Void = { } @@ -261,13 +261,13 @@ public final class WebSearchController: ViewController { selectionState.setItem(currentItem, selected: true) } if case let .media(_, sendSelected) = mode { - sendSelected(results, selectionState, editingState, false) + sendSelected(results, selectionState, editingState, silently, scheduleTime) } } }, schedule: { [weak self] messageEffect in if let strongSelf = self { - strongSelf.presentSchedulePicker(false, { [weak self] time in - self?.controllerInteraction?.sendSelected(nil, false, time, nil) + strongSelf.presentSchedulePicker(false, { [weak self] time, silentPosting in + self?.controllerInteraction?.sendSelected(nil, silentPosting, time, nil) }) } }, avatarCompleted: { result in