Various improvements

This commit is contained in:
Ilya Laktyushin 2026-04-24 03:16:14 +02:00
parent 32b2ede5aa
commit f41630083a
77 changed files with 1303 additions and 685 deletions

View file

@ -16208,3 +16208,11 @@ Error: %8$@";
"Settings.Birthday.PrivacyHelpEveryone" = "Everyone can see your birthday. [Change >]()"; "Settings.Birthday.PrivacyHelpEveryone" = "Everyone can see your birthday. [Change >]()";
"Settings.Birthday.PrivacyHelpContacts" = "Only your contacts 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 >]()"; "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.";

View file

@ -9,7 +9,7 @@ public protocol ContactSelectionController: ViewController {
var result: Signal<([ContactListPeer], ContactListAction, Bool, Int32?, NSAttributedString?, ChatSendMessageActionSheetController.SendParameters?)?, NoError> { get } var result: Signal<([ContactListPeer], ContactListAction, Bool, Int32?, NSAttributedString?, ChatSendMessageActionSheetController.SendParameters?)?, NoError> { get }
var displayProgress: Bool { get set } var displayProgress: Bool { get set }
var dismissed: (() -> Void)? { 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() func dismissSearch()
} }

View file

@ -997,7 +997,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
controller.reset = { [weak self, weak controller] in controller.reset = { [weak self, weak controller] in
if let strongSelf = self, let strongController = controller { 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: [ 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: { TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Login_ResetAccountProtected_Reset, action: {
if let strongSelf = self, let strongController = controller { if let strongSelf = self, let strongController = controller {
strongController.inProgress = true strongController.inProgress = true
@ -1084,7 +1084,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
controller.reset = { [weak self, weak controller] in controller.reset = { [weak self, weak controller] in
if let strongSelf = self, let strongController = controller { if let strongSelf = self, let strongController = controller {
strongController.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_ResetAccountConfirmation, actions: [ 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: { TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Login_ResetAccountProtected_Reset, action: {
if let strongSelf = self, let strongController = controller { if let strongSelf = self, let strongController = controller {
strongController.inProgress = true strongController.inProgress = true

View file

@ -168,7 +168,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component {
title: nil, title: nil,
text: errorText, text: errorText,
actions: [ 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 TextAlertAction(type: .defaultAction, title: presentationData.strings.Login_PhoneNumberHelp, action: { [weak self] in
guard let self else { guard let self else {
return return

View file

@ -31,6 +31,7 @@ swift_library(
"//submodules/TelegramUI/Components/MinimizedContainer", "//submodules/TelegramUI/Components/MinimizedContainer",
"//submodules/Pasteboard", "//submodules/Pasteboard",
"//submodules/SaveToCameraRoll", "//submodules/SaveToCameraRoll",
"//submodules/TextFormat:TextFormat",
"//submodules/TelegramUI/Components/NavigationStackComponent", "//submodules/TelegramUI/Components/NavigationStackComponent",
"//submodules/LocationUI", "//submodules/LocationUI",
"//submodules/OpenInExternalAppUI", "//submodules/OpenInExternalAppUI",

View file

@ -20,6 +20,7 @@ import SafariServices
import LocationUI import LocationUI
import OpenInExternalAppUI import OpenInExternalAppUI
import GalleryUI import GalleryUI
import TextFormat
final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDelegate { final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDelegate {
private let context: AccountContext private let context: AccountContext
@ -67,6 +68,8 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
var currentDetailsItems: [InstantPageDetailsItem] = [] var currentDetailsItems: [InstantPageDetailsItem] = []
private var resolvedExternalMediaDimensions: [MediaId: PixelDimensions] = [:] private var resolvedExternalMediaDimensions: [MediaId: PixelDimensions] = [:]
private var pendingResolvedExternalMediaDimensions = Set<MediaId>() private var pendingResolvedExternalMediaDimensions = Set<MediaId>()
private var codeHighlight: CachedMessageSyntaxHighlight?
private var codeHighlightState: (specs: [CachedMessageSyntaxHighlight.Spec], disposable: Disposable)?
var currentAccessibilityAreas: [AccessibilityAreaNode] = [] var currentAccessibilityAreas: [AccessibilityAreaNode] = []
@ -90,6 +93,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
private let resolveUrlDisposable = MetaDisposable() private let resolveUrlDisposable = MetaDisposable()
private let updateLayoutDisposable = MetaDisposable() private let updateLayoutDisposable = MetaDisposable()
private let updateExternalMediaDimensionsDisposable = MetaDisposable() private let updateExternalMediaDimensionsDisposable = MetaDisposable()
private let updateCodeHighlightDisposable = MetaDisposable()
private let loadProgress = ValuePromise<CGFloat>(1.0, ignoreRepeated: true) private let loadProgress = ValuePromise<CGFloat>(1.0, ignoreRepeated: true)
private let readingProgress = ValuePromise<CGFloat>(0.0, ignoreRepeated: true) private let readingProgress = ValuePromise<CGFloat>(0.0, ignoreRepeated: true)
@ -190,6 +194,8 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
self.updateWebPage(result, anchor: self.initialAnchor) self.updateWebPage(result, anchor: self.initialAnchor)
}) })
} }
self.updateCodeHighlight()
} }
deinit { deinit {
@ -199,6 +205,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
self.resolveUrlDisposable.dispose() self.resolveUrlDisposable.dispose()
self.updateLayoutDisposable.dispose() self.updateLayoutDisposable.dispose()
self.updateExternalMediaDimensionsDisposable.dispose() self.updateExternalMediaDimensionsDisposable.dispose()
self.updateCodeHighlightDisposable.dispose()
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
@ -325,6 +332,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
} }
} }
self.currentLayout = nil self.currentLayout = nil
self.updateCodeHighlight()
self.updatePageLayout() self.updatePageLayout()
self.scrollNode.frame = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0) 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 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) let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: size.width)
@ -551,6 +559,96 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
self.scrollNode.view.contentSize = currentLayout.contentSize 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)) 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<CachedMessageSyntaxHighlight.Spec>()
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) { func updateVisibleItems(visibleBounds: CGRect, animated: Bool = false) {
var visibleTileIndices = Set<Int>() var visibleTileIndices = Set<Int>()

View file

@ -330,7 +330,7 @@ private func markdownBlocks(from node: MarkdownIntentNode, context: MarkdownConv
} else if level == 2 { } else if level == 2 {
return [.header(text)] return [.header(text)]
} else { } else {
return [.subheader(text)] return [.heading(text: text, level: Int32(max(3, min(level, 6))))]
} }
case .paragraph: case .paragraph:
let inlineContent = markdownInlineContent(from: node.attributedText, context: context) let inlineContent = markdownInlineContent(from: node.attributedText, context: context)
@ -349,12 +349,12 @@ private func markdownBlocks(from node: MarkdownIntentNode, context: MarkdownConv
return [] return []
} }
return [.paragraph(text)] return [.paragraph(text)]
case .codeBlock: case let .codeBlock(languageHint):
let text = markdownRichText(from: markdownTrimTrailingCodeBlockNewline(node.attributedText), context: context) let text = markdownRichText(from: markdownTrimTrailingCodeBlockNewline(node.attributedText), context: context)
guard markdownHasDisplayableContent(text) else { guard markdownHasDisplayableContent(text) else {
return [] return []
} }
return [.preformatted(text)] return [.preformatted(text: text, language: markdownNormalizedCodeBlockLanguage(languageHint))]
case .thematicBreak: case .thematicBreak:
return [.divider] return [.divider]
case .blockQuote: case .blockQuote:
@ -897,9 +897,11 @@ private func markdownPlainText(from block: InstantPageBlock) -> String {
return text.plainText return text.plainText
case let .subheader(text): case let .subheader(text):
return text.plainText return text.plainText
case let .heading(text, _):
return text.plainText
case let .paragraph(text): case let .paragraph(text):
return text.plainText return text.plainText
case let .preformatted(text): case let .preformatted(text, _):
return text.plainText return text.plainText
case let .footer(text): case let .footer(text):
return text.plainText return text.plainText
@ -940,6 +942,14 @@ private func markdownTitle(from blocks: [InstantPageBlock], file: FileMediaRefer
return fileURL.lastPathComponent 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? { private func markdownFirstParagraphText(from blocks: [InstantPageBlock]) -> String? {
for block in blocks { for block in blocks {
switch block { switch block {
@ -1007,6 +1017,8 @@ private func markdownHeadingText(from block: InstantPageBlock) -> String? {
return text.plainText return text.plainText
case let .subheader(text): case let .subheader(text):
return text.plainText return text.plainText
case let .heading(text, _):
return text.plainText
default: default:
return nil return nil
} }

View file

@ -747,10 +747,16 @@ private func parsePageBlocks(_ input: [Any], _ url: String, _ media: inout [Medi
result.append(.paragraph(trim(parseRichText(item, &media)))) result.append(.paragraph(trim(parseRichText(item, &media))))
case "h1", "h2": case "h1", "h2":
result.append(.header(trim(parseRichText(item, &media)))) result.append(.header(trim(parseRichText(item, &media))))
case "h3", "h4", "h5", "h6": case "h3":
result.append(.subheader(trim(parseRichText(item, &media)))) 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": case "pre":
result.append(.preformatted(.fixed(trim(parseRichText(item, &media))))) result.append(.preformatted(text: .fixed(trim(parseRichText(item, &media))), language: nil))
case "blockquote": case "blockquote":
result.append(.blockQuote(text: .italic(trim(parseRichText(item, &media))), caption: .empty)) result.append(.blockQuote(text: .italic(trim(parseRichText(item, &media))), caption: .empty))
case "img": case "img":

View file

@ -163,6 +163,19 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
return false 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 hasDownloads: Bool = false
private var activeDownloadsDisposable: Disposable? private var activeDownloadsDisposable: Disposable?
@ -1015,7 +1028,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if isDisabled { if isDisabled {
let context = self.context let context = self.context
var replaceImpl: ((ViewController) -> Void)? 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) let controller = PremiumIntroScreen(context: context, source: .folders)
replaceImpl?(controller) replaceImpl?(controller)
return true return true
@ -1059,7 +1072,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if isDisabled { if isDisabled {
let context = self.context let context = self.context
var replaceImpl: ((ViewController) -> Void)? 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) let controller = PremiumIntroScreen(context: context, source: .folders)
replaceImpl?(controller) replaceImpl?(controller)
return true return true
@ -2377,7 +2390,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
} }
let context = strongSelf.context let context = strongSelf.context
var replaceImpl: ((ViewController) -> Void)? 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) let controller = PremiumIntroScreen(context: context, source: .folders)
replaceImpl?(controller) replaceImpl?(controller)
return true return true
@ -4563,7 +4576,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: { TextAlertAction(type: .destructiveAction, title: presentationData.strings.Common_Delete, action: {
confirmDeleteFolder() confirmDeleteFolder()
}), }),
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: { TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
}) })
]), in: .window(.root)) ]), in: .window(.root))
} else { } else {
@ -6251,7 +6264,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
if isDisabled { if isDisabled {
let context = strongSelf.context let context = strongSelf.context
var replaceImpl: ((ViewController) -> Void)? 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) let controller = PremiumIntroScreen(context: context, source: .folders)
replaceImpl?(controller) replaceImpl?(controller)
return true return true

View file

@ -1504,6 +1504,24 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
if self.controller?.tabContainerData != nil || !panels.isEmpty { if self.controller?.tabContainerData != nil || !panels.isEmpty {
var tabs: AnyComponent<Empty>? var tabs: AnyComponent<Empty>?
if let tabContainerData = self.controller?.tabContainerData, tabContainerData.0.count > 1 { 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 let selectedTab: HorizontalTabsComponent.Tab.Id
switch self.effectiveContainerNode.currentItemFilter { switch self.effectiveContainerNode.currentItemFilter {
case .all: case .all:
@ -1553,10 +1571,9 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
var isDisabled = false var isDisabled = false
if let filtersLimit = tabContainerData.2 { if let filtersLimit = tabContainerData.2 {
guard let folderIndex = tabContainerData.0.firstIndex(where: { $0.id == mappedId }) else { if let folderIndex = folderFilterIndex(mappedId, tabContainerData.0) {
return isDisabled = !isPremium && folderIndex >= filtersLimit
} }
isDisabled = !isPremium && folderIndex >= filtersLimit
} }
if isDisabled { if isDisabled {
@ -1599,10 +1616,9 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
var isDisabled = false var isDisabled = false
if let filtersLimit = tabContainerData.2 { if let filtersLimit = tabContainerData.2 {
guard let folderIndex = tabContainerData.0.firstIndex(where: { $0.id == entry.id }) else { if let folderIndex = folderFilterIndex(entry.id, tabContainerData.0) {
return isDisabled = !isPremium && folderIndex >= filtersLimit
} }
isDisabled = !isPremium && folderIndex >= filtersLimit
} }
self.controller?.tabContextGesture(id: mappedId, sourceNode: nil, sourceView: sourceView, gesture: gesture, keepInPlace: false, isDisabled: isDisabled) self.controller?.tabContextGesture(id: mappedId, sourceNode: nil, sourceView: sourceView, gesture: gesture, keepInPlace: false, isDisabled: isDisabled)

View file

@ -57,6 +57,18 @@ public extension UnicodeScalar {
private final class FrameworkClass: NSObject { private final class FrameworkClass: NSObject {
} }
private let allowedEmojiLikeSymbols: Set<String> = [
"\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 { public extension String {
func trimmingTrailingSpaces() -> String { func trimmingTrailingSpaces() -> String {
var t = self var t = self
@ -74,6 +86,20 @@ public extension String {
return self.contains { $0.isEmoji } 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 { var containsOnlyEmoji: Bool {
return !self.isEmpty && !self.contains { !$0.isEmoji } return !self.isEmpty && !self.contains { !$0.isEmoji }
} }

View file

@ -17,6 +17,7 @@ swift_library(
"//submodules/TelegramCore:TelegramCore", "//submodules/TelegramCore:TelegramCore",
"//submodules/TelegramPresentationData:TelegramPresentationData", "//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/TelegramUIPreferences:TelegramUIPreferences", "//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/TextFormat:TextFormat",
"//submodules/GalleryUI:GalleryUI", "//submodules/GalleryUI:GalleryUI",
"//submodules/MusicAlbumArtResources:MusicAlbumArtResources", "//submodules/MusicAlbumArtResources:MusicAlbumArtResources",
"//submodules/LiveLocationPositionNode:LiveLocationPositionNode", "//submodules/LiveLocationPositionNode:LiveLocationPositionNode",

View file

@ -6,6 +6,7 @@ import TelegramPresentationData
import TelegramUIPreferences import TelegramUIPreferences
import TelegramStringFormatting import TelegramStringFormatting
import MosaicLayout import MosaicLayout
import TextFormat
public final class InstantPageLayout { public final class InstantPageLayout {
public let origin: CGPoint public let origin: CGPoint
@ -28,8 +29,7 @@ public final class InstantPageLayout {
} }
} }
private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantPageTheme, category: InstantPageTextCategoryType, link: Bool) { private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantPageTheme, attributes: InstantPageTextAttributes) {
let attributes = theme.textCategories.attributes(type: category, link: link)
stack.push(.textColor(attributes.color)) stack.push(.textColor(attributes.color))
stack.push(.markerColor(theme.markerColor)) stack.push(.markerColor(theme.markerColor))
stack.push(.linkColor(theme.linkColor)) 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 let layoutCaption: (InstantPageCaption, CGSize) -> ([InstantPageItem], CGSize) = { caption, contentSize in
var items: [InstantPageItem] = [] var items: [InstantPageItem] = []
@ -100,7 +184,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
switch block { switch block {
case let .cover(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): case let .title(text):
let styleStack = InstantPageTextStyleStack() let styleStack = InstantPageTextStyleStack()
setupStyleStack(styleStack, theme: theme, category: .header, link: false) 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) 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) 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) 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): case let .paragraph(text):
let styleStack = InstantPageTextStyleStack() let styleStack = InstantPageTextStyleStack()
setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false) 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) 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) return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
case let .preformatted(text): case let .preformatted(text, language):
let styleStack = InstantPageTextStyleStack()
setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false)
let backgroundInset: CGFloat = 14.0 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) 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] var allItems: [InstantPageItem] = [backgroundItem]
allItems.append(contentsOf: items) allItems.append(contentsOf: items)
@ -274,7 +376,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var previousBlock: InstantPageBlock? var previousBlock: InstantPageBlock?
var originY: CGFloat = contentSize.height var originY: CGFloat = contentSize.height
for subBlock in blocks { 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 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)) 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 var i = 0
for subItem in innerItems { for subItem in innerItems {
let frame = mosaicLayout[i].0 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)) items.append(contentsOf: subLayout.flattenedItemsWithOrigin(frame.origin))
i += 1 i += 1
} }
@ -550,7 +652,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var previousBlock: InstantPageBlock? var previousBlock: InstantPageBlock?
for subBlock in blocks { 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 spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock)
let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: horizontalInset + lineInset, y: contentSize.height + spacing)) 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? var previousBlock: InstantPageBlock?
for subBlock in blocks { 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 spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock)
let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing)) 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? var maybeLoadedContent: TelegramMediaWebpageLoadedContent?
if case let .Loaded(content) = webPage.content { if case let .Loaded(content) = webPage.content {
maybeLoadedContent = content maybeLoadedContent = content
@ -871,7 +973,7 @@ public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, instant
var previousBlock: InstantPageBlock? var previousBlock: InstantPageBlock?
for block in pageBlocks { 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 spacing = spacingBetweenBlocks(upper: previousBlock, lower: block)
let blockItems = blockLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing)) let blockItems = blockLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing))
items.append(contentsOf: blockItems) items.append(contentsOf: blockItems)

View file

@ -21,7 +21,7 @@ func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?) ->
return 20.0 return 20.0
case (.title, .paragraph), (.authorDate, .paragraph): case (.title, .paragraph), (.authorDate, .paragraph):
return 34.0 return 34.0
case (.header, .paragraph), (.subheader, .paragraph): case (.header, .paragraph), (.subheader, .paragraph), (.heading, .paragraph):
return 25.0 return 25.0
case (.list, .paragraph): case (.list, .paragraph):
return 31.0 return 31.0
@ -33,7 +33,7 @@ func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?) ->
return 20.0 return 20.0
case (.title, .list), (.authorDate, .list): case (.title, .list), (.authorDate, .list):
return 34.0 return 34.0
case (.header, .list), (.subheader, .list): case (.header, .list), (.subheader, .list), (.heading, .list):
return 31.0 return 31.0
case (.preformatted, .list): case (.preformatted, .list):
return 19.0 return 19.0
@ -43,7 +43,7 @@ func spacingBetweenBlocks(upper: InstantPageBlock?, lower: InstantPageBlock?) ->
return 19.0 return 19.0
case (_, .preformatted): case (_, .preformatted):
return 20.0 return 20.0
case (_, .header), (_, .subheader): case (_, .header), (_, .subheader), (_, .heading):
return 32.0 return 32.0
default: default:
return 20.0 return 20.0

View file

@ -135,6 +135,32 @@ public final class InstantPageTheme {
public func withUpdatedFontStyles(sizeMultiplier: CGFloat, forceSerif: Bool) -> 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) 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( private let lightTheme = InstantPageTheme(

View file

@ -35,7 +35,7 @@
@property (nonatomic) bool reminder; @property (nonatomic) bool reminder;
@property (nonatomic) bool forum; @property (nonatomic) bool forum;
@property (nonatomic) bool isSuggesting; @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, copy) void (^presentTimerController)(void (^)(int32_t));
@property (nonatomic, strong) NSArray *underlyingViews; @property (nonatomic, strong) NSArray *underlyingViews;

View file

@ -63,7 +63,7 @@ typedef enum {
@property (nonatomic, copy) void(^finishedTransitionOut)(void); @property (nonatomic, copy) void(^finishedTransitionOut)(void);
@property (nonatomic, copy) void(^customPresentOverlayController)(TGOverlayController *(^)(id<LegacyComponentsContext>)); @property (nonatomic, copy) void(^customPresentOverlayController)(TGOverlayController *(^)(id<LegacyComponentsContext>));
@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, copy) void (^presentTimerController)(void (^)(int32_t));
- (instancetype)initWithContext:(id<LegacyComponentsContext>)context saveEditedPhotos:(bool)saveEditedPhotos saveCapturedMedia:(bool)saveCapturedMedia; - (instancetype)initWithContext:(id<LegacyComponentsContext>)context saveEditedPhotos:(bool)saveEditedPhotos saveCapturedMedia:(bool)saveCapturedMedia;

View file

@ -70,7 +70,7 @@ typedef enum
@property (nonatomic, assign) bool forum; @property (nonatomic, assign) bool forum;
@property (nonatomic, assign) bool isSuggesting; @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, copy) void (^presentTimerController)(void (^)(int32_t));
@property (nonatomic, assign) bool liveVideoUploadEnabled; @property (nonatomic, assign) bool liveVideoUploadEnabled;

View file

@ -35,7 +35,7 @@
@property (nonatomic, assign) bool forum; @property (nonatomic, assign) bool forum;
@property (nonatomic, assign) bool isSuggesting; @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, copy) void (^presentTimerController)(void (^)(int32_t));
@property (nonatomic, assign) CGFloat topInset; @property (nonatomic, assign) CGFloat topInset;

View file

@ -28,7 +28,7 @@
@property (nonatomic, copy) void (^editorOpened)(void); @property (nonatomic, copy) void (^editorOpened)(void);
@property (nonatomic, copy) void (^editorClosed)(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)); @property (nonatomic, copy) void (^presentTimerController)(void (^)(int32_t));
- (instancetype)initWithContext:(id<LegacyComponentsContext>)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<TGPhotoPaintStickersContext>)stickersContext; - (instancetype)initWithContext:(id<LegacyComponentsContext>)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<TGPhotoPaintStickersContext>)stickersContext;

View file

@ -9,7 +9,7 @@
@class TGModernGalleryController; @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 TGPhotoVideoEditorSchedulePicker)(bool media, TGPhotoVideoEditorSchedulePickerCompletion _Nonnull done);
typedef void (^ _Nonnull TGPhotoVideoEditorCompletion)(id<TGMediaEditableItem> _Nonnull item, TGMediaEditingContext * _Nonnull editingContext, bool silentPosting, int32_t scheduleTime); typedef void (^ _Nonnull TGPhotoVideoEditorCompletion)(id<TGMediaEditableItem> _Nonnull item, TGMediaEditingContext * _Nonnull editingContext, bool silentPosting, int32_t scheduleTime);

View file

@ -31,7 +31,7 @@
@property (nonatomic, copy) void(^didDismiss)(void); @property (nonatomic, copy) void(^didDismiss)(void);
@property (nonatomic, copy) void(^didStop)(void); @property (nonatomic, copy) void(^didStop)(void);
@property (nonatomic, copy) void(^displaySlowmodeTooltip)(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<LegacyComponentsContext>)context forStory:(bool)forStory assets:(TGVideoMessageCaptureControllerAssets *)assets transitionInView:(UIView *(^)(void))transitionInView parentController:(TGViewController *)parentController controlsFrame:(CGRect)controlsFrame isAlreadyLocked:(bool (^)(void))isAlreadyLocked liveUploadInterface:(id<TGLiveUploadInterface>)liveUploadInterface pallete:(TGModernConversationInputMicPallete *)pallete slowmodeTimestamp:(int32_t)slowmodeTimestamp slowmodeView:(UIView *(^)(void))slowmodeView canSendSilently:(bool)canSendSilently canSchedule:(bool)canSchedule reminder:(bool)reminder; - (instancetype)initWithContext:(id<LegacyComponentsContext>)context forStory:(bool)forStory assets:(TGVideoMessageCaptureControllerAssets *)assets transitionInView:(UIView *(^)(void))transitionInView parentController:(TGViewController *)parentController controlsFrame:(CGRect)controlsFrame isAlreadyLocked:(bool (^)(void))isAlreadyLocked liveUploadInterface:(id<TGLiveUploadInterface>)liveUploadInterface pallete:(TGModernConversationInputMicPallete *)pallete slowmodeTimestamp:(int32_t)slowmodeTimestamp slowmodeView:(UIView *(^)(void))slowmodeView canSendSilently:(bool)canSendSilently canSchedule:(bool)canSchedule reminder:(bool)reminder;

View file

@ -1656,7 +1656,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
if (strongSelf == nil) if (strongSelf == nil)
return; return;
strongSelf.presentScheduleController(true, ^(int32_t time) { strongSelf.presentScheduleController(true, ^(int32_t time, bool silentPosting) {
__strong TGCameraController *strongSelf = weakSelf; __strong TGCameraController *strongSelf = weakSelf;
__strong TGMediaPickerGalleryModel *strongModel = weakModel; __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"]; [[NSUserDefaults standardUserDefaults] setObject:@(!strongSelf->_selectionContext.grouping) forKey:@"TG_mediaGroupingDisabled_v0"];
if (strongSelf.finishedWithResults != nil) 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]; [strongSelf _dismissTransitionForResultController:strongController];
}); });

View file

@ -478,7 +478,7 @@ static TGVideoEditAdjustments *TGMediaAssetsPatchedLivePhotoAdjustments(PGPhotoE
self.pickerController.isSuggesting = isSuggesting; self.pickerController.isSuggesting = isSuggesting;
} }
- (void)setPresentScheduleController:(void (^)(bool, void (^)(int32_t)))presentScheduleController { - (void)setPresentScheduleController:(void (^)(bool, void (^)(int32_t, bool)))presentScheduleController {
_presentScheduleController = [presentScheduleController copy]; _presentScheduleController = [presentScheduleController copy];
self.pickerController.presentScheduleController = presentScheduleController; self.pickerController.presentScheduleController = presentScheduleController;
} }
@ -1890,8 +1890,8 @@ static TGVideoEditAdjustments *TGMediaAssetsPatchedLivePhotoAdjustments(PGPhotoE
- (void)schedule:(bool)media { - (void)schedule:(bool)media {
__weak TGMediaAssetsController *weakSelf = self; __weak TGMediaAssetsController *weakSelf = self;
self.presentScheduleController(media, ^(int32_t scheduleTime) { self.presentScheduleController(media, ^(int32_t scheduleTime, bool silentPosting) {
[weakSelf completeWithCurrentItem:nil silentPosting:false scheduleTime:scheduleTime]; [weakSelf completeWithCurrentItem:nil silentPosting:silentPosting scheduleTime:scheduleTime];
}); });
} }

View file

@ -201,7 +201,7 @@
if (strongSelf == nil) if (strongSelf == nil)
return; return;
strongSelf.presentScheduleController(true, ^(int32_t time) { strongSelf.presentScheduleController(true, ^(int32_t time, bool silentPosting) {
__strong TGMediaPickerModernGalleryMixin *strongSelf = weakSelf; __strong TGMediaPickerModernGalleryMixin *strongSelf = weakSelf;
if (strongSelf == nil) if (strongSelf == nil)
return; return;
@ -209,7 +209,7 @@
strongSelf->_galleryModel.dismiss(true, false); strongSelf->_galleryModel.dismiss(true, false);
if (strongSelf.completeWithItem != nil) if (strongSelf.completeWithItem != nil)
strongSelf.completeWithItem(item, false, time); strongSelf.completeWithItem(item, silentPosting, time);
}); });
}; };
controller.sendWithTimer = ^{ controller.sendWithTimer = ^{

View file

@ -282,8 +282,8 @@
complete(false, 0x7ffffffe); complete(false, 0x7ffffffe);
}; };
sendController.schedule = ^{ sendController.schedule = ^{
presentSchedulePicker(true, ^(int32_t time) { presentSchedulePicker(true, ^(int32_t time, bool silentPosting) {
complete(false, time); complete(silentPosting, time);
}); });
}; };
[strongController presentViewController:sendController animated:false completion:nil]; [strongController presentViewController:sendController animated:false completion:nil];

View file

@ -844,13 +844,13 @@ typedef enum
} }
if (strongSelf.presentScheduleController) { if (strongSelf.presentScheduleController) {
strongSelf.presentScheduleController(^(int32_t time) { strongSelf.presentScheduleController(^(int32_t time, bool silentPosting) {
__strong TGVideoMessageCaptureController *strongSelf = weakSelf; __strong TGVideoMessageCaptureController *strongSelf = weakSelf;
if (strongSelf == nil) { if (strongSelf == nil) {
return; 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; _automaticDismiss = true;
[strongSelf dismiss:false]; [strongSelf dismiss:false];

View file

@ -170,7 +170,7 @@ public func legacyMediaEditor(
hasSilentPosting: Bool = false, hasSilentPosting: Bool = false,
hasSchedule: Bool = false, hasSchedule: Bool = false,
reminder: 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, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, Bool) -> Void,
present: @escaping (ViewController, Any?) -> Void present: @escaping (ViewController, Any?) -> Void
) { ) {
@ -213,7 +213,7 @@ public func legacyMediaEditor(
legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true 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) presentSchedulePicker(media, done)
} }
let appeared: () -> Void = { let appeared: () -> Void = {
@ -322,7 +322,7 @@ public func legacyAttachmentMenu(
presentSelectionLimitExceeded: @escaping () -> Void, presentSelectionLimitExceeded: @escaping () -> Void,
presentCantSendMultipleFiles: @escaping () -> Void, presentCantSendMultipleFiles: @escaping () -> Void,
presentJpegConversionAlert: @escaping (@escaping (Bool) -> Void) -> 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, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void,
sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ((String) -> UIView?)?, @escaping () -> Void) -> Void, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ((String) -> UIView?)?, @escaping () -> Void) -> Void,
selectRecentlyUsedInlineBot: @escaping (Peer) -> Void, selectRecentlyUsedInlineBot: @escaping (Peer) -> Void,
@ -431,8 +431,8 @@ public func legacyAttachmentMenu(
carouselItem.hasSchedule = hasSchedule carouselItem.hasSchedule = hasSchedule
carouselItem.reminder = peer?.id == context.account.peerId carouselItem.reminder = peer?.id == context.account.peerId
carouselItem.presentScheduleController = { media, done in carouselItem.presentScheduleController = { media, done in
presentSchedulePicker(media, { time in presentSchedulePicker(media, { time, silentPosting in
done?(time) done?(time, silentPosting)
}) })
} }
carouselItem.presentTimerController = { done in carouselItem.presentTimerController = { done in

View file

@ -20,7 +20,7 @@ public func guessMimeTypeByFileExtension(_ ext: String) -> String {
return TGMimeTypeMap.mimeType(forExtension: ext) ?? "application/binary" 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) let paintStickersContext = LegacyPaintStickersContext(context: context)
paintStickersContext.captionPanelView = { paintStickersContext.captionPanelView = {
return getCaptionPanelView() return getCaptionPanelView()
@ -43,8 +43,8 @@ public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, co
controller.hasCoverButton = true controller.hasCoverButton = true
} }
controller.presentScheduleController = { media, done in controller.presentScheduleController = { media, done in
presentSchedulePicker(media, { time in presentSchedulePicker(media, { time, silentPosting in
done?(time) done?(time, silentPosting)
}) })
} }
controller.presentTimerController = { done in controller.presentTimerController = { done in

View file

@ -131,7 +131,7 @@ func presentLegacyMediaPickerGallery(
transitionHostView: @escaping () -> UIView?, transitionHostView: @escaping () -> UIView?,
transitionView: @escaping (String) -> UIView?, transitionView: @escaping (String) -> UIView?,
completed: @escaping (TGMediaSelectableItem & TGMediaEditableItem, Bool, Int32?, @escaping () -> Void) -> Void, 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, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void,
getCaptionPanelView: @escaping () -> TGCaptionPanelView?, getCaptionPanelView: @escaping () -> TGCaptionPanelView?,
present: @escaping (ViewController, Any?) -> Void, present: @escaping (ViewController, Any?) -> Void,
@ -369,8 +369,8 @@ func presentLegacyMediaPickerGallery(
}) })
} }
sheetController.schedule = { sheetController.schedule = {
presentSchedulePicker(true, { time in presentSchedulePicker(true, { time, silentPosting in
completed(item.asset, false, time, { completed(item.asset, silentPosting, time, {
dismissImpl() dismissImpl()
}) })
}) })

View file

@ -248,7 +248,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
public weak var webSearchController: WebSearchController? public weak var webSearchController: WebSearchController?
public var openCamera: ((Any?) -> Void)? 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 presentTimerPicker: (@escaping (Int32) -> Void) -> Void = { _ in }
public var presentWebSearch: (MediaGroupsScreen, Bool) -> Void = { _, _ in } public var presentWebSearch: (MediaGroupsScreen, Bool) -> Void = { _, _ in }
public var getCaptionPanelView: () -> TGCaptionPanelView? = { return nil } public var getCaptionPanelView: () -> TGCaptionPanelView? = { return nil }
@ -2298,8 +2298,8 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
} }
}, schedule: { [weak self] parameters in }, schedule: { [weak self] parameters in
if let strongSelf = self { if let strongSelf = self {
strongSelf.presentSchedulePicker(false, { [weak self] time in strongSelf.presentSchedulePicker(false, { [weak self] time, silentPosting in
self?.interaction?.sendSelected(nil, false, time, true, parameters, {}) self?.interaction?.sendSelected(nil, silentPosting, time, true, parameters, {})
}) })
} }
}, dismissInput: { [weak self] in }, dismissInput: { [weak self] in

View file

@ -476,7 +476,7 @@ public func notificationSoundSelectionController(context: AccountContext, update
break break
} }
}), }),
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: { TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
}) })
], parseMarkdown: true), in: .window(.root)) ], parseMarkdown: true), in: .window(.root))
} }

View file

@ -697,7 +697,7 @@ final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode {
}, transition: .animated(duration: 0.5, curve: .spring)) }, transition: .animated(duration: 0.5, curve: .spring))
} }
if case let .enterEmail(enterEmailState, enterEmailEmail)? = self.innerState.data.state, case .create = enterEmailState, enterEmailEmail.isEmpty { 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() continueImpl()
})]), nil) })]), nil)
} else { } else {

View file

@ -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)) 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)) ]), in: .window(.root))
case let .passwordHint(recovery, password, doneText): case let .passwordHint(recovery, password, doneText):
if let recovery = recovery { if let recovery = recovery {
@ -550,7 +550,7 @@ public final class TwoFactorDataInputScreen: ViewController {
} }
strongSelf.performRecovery(recovery: recovery, password: "", hint: "") 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)) ]), in: .window(.root))
case let .rememberPassword(doneText): case let .rememberPassword(doneText):
guard case let .authorized(engine) = strongSelf.engine else { guard case let .authorized(engine) = strongSelf.engine else {

View file

@ -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 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) arguments.updateRank(text, updatedText)
}, shouldUpdateText: { text in }, shouldUpdateText: { text in
if text.containsEmoji { if text.containsGraphicEmoji {
arguments.animateError() arguments.animateError()
return false return false
} }
@ -1329,7 +1329,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD
return current return current
} }
if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsEmoji { if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsGraphicEmoji {
errorImpl?() errorImpl?()
return return
} }
@ -1361,7 +1361,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD
} }
let effectiveRank = updateRank ?? currentRank let effectiveRank = updateRank ?? currentRank
if effectiveRank?.containsEmoji ?? false { if effectiveRank?.containsGraphicEmoji ?? false {
errorImpl?() errorImpl?()
return return
} }
@ -1436,7 +1436,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD
return current return current
} }
if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsEmoji { if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsGraphicEmoji {
errorImpl?() errorImpl?()
return return
} }
@ -1516,7 +1516,7 @@ public func channelAdminController(context: AccountContext, updatedPresentationD
return current return current
} }
if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsEmoji { if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsGraphicEmoji {
errorImpl?() errorImpl?()
return return
} }

View file

@ -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 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) arguments.updateRank(text, updatedText)
}, shouldUpdateText: { text in }, shouldUpdateText: { text in
if text.containsEmoji { if text.containsGraphicEmoji {
arguments.animateError() arguments.animateError()
return false return false
} }
@ -860,7 +860,7 @@ public func channelBannedMemberController(context: AccountContext, updatedPresen
updateRank = current.updatedRank?.trimmingCharacters(in: .whitespacesAndNewlines) updateRank = current.updatedRank?.trimmingCharacters(in: .whitespacesAndNewlines)
return current return current
} }
if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsEmoji { if let updateRank = updateRank, updateRank.count > rankMaxLength || updateRank.containsGraphicEmoji {
errorImpl?() errorImpl?()
return return
} }

View file

@ -534,6 +534,8 @@ func stringForGroupPermission(strings: PresentationStrings, right: TelegramChatB
return strings.Channel_BanUser_PermissionSendVoiceMessage return strings.Channel_BanUser_PermissionSendVoiceMessage
} else if right.contains(.banSendInstantVideos) { } else if right.contains(.banSendInstantVideos) {
return strings.Channel_BanUser_PermissionSendVideoMessage return strings.Channel_BanUser_PermissionSendVideoMessage
} else if right.contains(.banSendReactions) {
return strings.Channel_BanUser_PermissionSendReactions
} else if right.contains(.banEditRank) { } else if right.contains(.banEditRank) {
if defaultPermissions { if defaultPermissions {
return strings.Channel_BanUser_PermissionEditOwnRank return strings.Channel_BanUser_PermissionEditOwnRank
@ -568,6 +570,8 @@ func compactStringForGroupPermission(strings: PresentationStrings, right: Telegr
return strings.GroupPermission_NoSendLinks return strings.GroupPermission_NoSendLinks
} else if right.contains(.banSendPolls) { } else if right.contains(.banSendPolls) {
return strings.GroupPermission_NoSendPolls return strings.GroupPermission_NoSendPolls
} else if right.contains(.banSendReactions) {
return strings.GroupPermission_NoSendReactions
} else if right.contains(.banChangeInfo) { } else if right.contains(.banChangeInfo) {
return strings.GroupPermission_NoChangeInfo return strings.GroupPermission_NoChangeInfo
} else if right.contains(.banAddMembers) { } else if right.contains(.banAddMembers) {
@ -595,6 +599,7 @@ private let internal_allPossibleGroupPermissionList: [(TelegramChatBannedRightsF
(.banSendInstantVideos, .banMembers), (.banSendInstantVideos, .banMembers),
(.banEmbedLinks, .banMembers), (.banEmbedLinks, .banMembers),
(.banSendPolls, .banMembers), (.banSendPolls, .banMembers),
(.banSendReactions, .banMembers),
(.banAddMembers, .banMembers), (.banAddMembers, .banMembers),
(.banPinMessages, .pinMessages), (.banPinMessages, .pinMessages),
(.banManageTopics, .manageTopics), (.banManageTopics, .manageTopics),
@ -647,6 +652,7 @@ public func banSendMediaSubList() -> [(TelegramChatBannedRightsFlags, TelegramCh
(.banSendInstantVideos, .banMembers), (.banSendInstantVideos, .banMembers),
(.banEmbedLinks, .banMembers), (.banEmbedLinks, .banMembers),
(.banSendPolls, .banMembers), (.banSendPolls, .banMembers),
(.banSendReactions, .banMembers)
] ]
} }

View file

@ -313,7 +313,7 @@ func createPasswordController(context: AccountContext, createPasswordContext: Cr
} }
if emailAlert { 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() saveImpl()
})]), nil) })]), nil)
} else { } else {

View file

@ -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: { strongSelf.present(textAlertController(context: strongSelf.context, title: strongSelf.presentationData.strings.PrivacyPolicy_Decline, text: text, actions: [TextAlertAction(type: .destructiveAction, title: declineTitle, action: {
self?.decline() 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)) })], actionLayout: .vertical), in: .window(.root))
}, rightAction: { [weak self] in }, rightAction: { [weak self] in
guard let strongSelf = self else { guard let strongSelf = self else {

View file

@ -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<Api.Updates>) {
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<Api.Bool>) {
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 { public extension Api.functions.messages {
static func deletePhoneCallHistory(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.AffectedFoundMessages>) { static func deletePhoneCallHistory(flags: Int32) -> (FunctionDescription, Buffer, DeserializeFunctionResponse<Api.messages.AffectedFoundMessages>) {
let buffer = Buffer() let buffer = Buffer()

View file

@ -34,7 +34,8 @@ union InstantPageBlock_Value {
InstantPageBlock_Table, InstantPageBlock_Table,
InstantPageBlock_Details, InstantPageBlock_Details,
InstantPageBlock_RelatedArticles, InstantPageBlock_RelatedArticles,
InstantPageBlock_Map InstantPageBlock_Map,
InstantPageBlock_Heading
} }
table InstantPageBlock { table InstantPageBlock {
@ -70,6 +71,7 @@ table InstantPageBlock_Paragraph {
table InstantPageBlock_Preformatted { table InstantPageBlock_Preformatted {
text:RichText (id: 0, required); text:RichText (id: 0, required);
language:string (id: 1);
} }
table InstantPageBlock_Footer { table InstantPageBlock_Footer {
@ -184,6 +186,11 @@ table InstantPageBlock_Map {
caption:InstantPageCaption (id: 4, required); caption:InstantPageCaption (id: 4, required);
} }
table InstantPageBlock_Heading {
text:RichText (id: 0, required);
level:int32 (id: 1);
}
table InstantPageCaption { table InstantPageCaption {
text:RichText (id: 0, required); text:RichText (id: 0, required);
credit:RichText (id: 1, required); credit:RichText (id: 1, required);

View file

@ -121,7 +121,7 @@ extension InstantPageBlock {
self = .paragraph(RichText(apiText: text)) self = .paragraph(RichText(apiText: text))
case let .pageBlockPreformatted(pageBlockPreformattedData): case let .pageBlockPreformatted(pageBlockPreformattedData):
let text = pageBlockPreformattedData.text let text = pageBlockPreformattedData.text
self = .preformatted(RichText(apiText: text)) self = .preformatted(text: RichText(apiText: text), language: nil)
case let .pageBlockFooter(pageBlockFooterData): case let .pageBlockFooter(pageBlockFooterData):
let text = pageBlockFooterData.text let text = pageBlockFooterData.text
self = .footer(RichText(apiText: text)) self = .footer(RichText(apiText: text))

View file

@ -32,6 +32,7 @@ private enum InstantPageBlockType: Int32 {
case details = 25 case details = 25
case relatedArticles = 26 case relatedArticles = 26
case map = 27 case map = 27
case heading = 28
} }
private func decodeListItems(_ decoder: PostboxDecoder) -> [InstantPageListItem] { private func decodeListItems(_ decoder: PostboxDecoder) -> [InstantPageListItem] {
@ -60,8 +61,9 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable {
case authorDate(author: RichText, date: Int32) case authorDate(author: RichText, date: Int32)
case header(RichText) case header(RichText)
case subheader(RichText) case subheader(RichText)
case heading(text: RichText, level: Int32)
case paragraph(RichText) case paragraph(RichText)
case preformatted(RichText) case preformatted(text: RichText, language: String?)
case footer(RichText) case footer(RichText)
case divider case divider
case anchor(String) case anchor(String)
@ -97,10 +99,15 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable {
self = .header(decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText) self = .header(decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText)
case InstantPageBlockType.subheader.rawValue: case InstantPageBlockType.subheader.rawValue:
self = .subheader(decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText) 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: case InstantPageBlockType.paragraph.rawValue:
self = .paragraph(decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText) self = .paragraph(decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText)
case InstantPageBlockType.preformatted.rawValue: 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: case InstantPageBlockType.footer.rawValue:
self = .footer(decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText) self = .footer(decoder.decodeObjectForKey("t", decoder: { RichText(decoder: $0) }) as! RichText)
case InstantPageBlockType.divider.rawValue: case InstantPageBlockType.divider.rawValue:
@ -184,12 +191,21 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable {
case let .subheader(text): case let .subheader(text):
encoder.encodeInt32(InstantPageBlockType.subheader.rawValue, forKey: "r") encoder.encodeInt32(InstantPageBlockType.subheader.rawValue, forKey: "r")
encoder.encodeObject(text, forKey: "t") 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): case let .paragraph(text):
encoder.encodeInt32(InstantPageBlockType.paragraph.rawValue, forKey: "r") encoder.encodeInt32(InstantPageBlockType.paragraph.rawValue, forKey: "r")
encoder.encodeObject(text, forKey: "t") encoder.encodeObject(text, forKey: "t")
case let .preformatted(text): case let .preformatted(text, language):
encoder.encodeInt32(InstantPageBlockType.preformatted.rawValue, forKey: "r") encoder.encodeInt32(InstantPageBlockType.preformatted.rawValue, forKey: "r")
encoder.encodeObject(text, forKey: "t") encoder.encodeObject(text, forKey: "t")
if let language {
encoder.encodeString(language, forKey: "l")
} else {
encoder.encodeNil(forKey: "l")
}
case let .footer(text): case let .footer(text):
encoder.encodeInt32(InstantPageBlockType.footer.rawValue, forKey: "r") encoder.encodeInt32(InstantPageBlockType.footer.rawValue, forKey: "r")
encoder.encodeObject(text, forKey: "t") encoder.encodeObject(text, forKey: "t")
@ -374,14 +390,20 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable {
} else { } else {
return false 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): case let .paragraph(text):
if case .paragraph(text) = rhs { if case .paragraph(text) = rhs {
return true return true
} else { } else {
return false return false
} }
case let .preformatted(text): case let .preformatted(lhsText, lhsLanguage):
if case .preformatted(text) = rhs { if case let .preformatted(rhsText, rhsLanguage) = rhs, lhsText == rhsText, lhsLanguage == rhsLanguage {
return true return true
} else { } else {
return false return false
@ -545,6 +567,11 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable {
throw FlatBuffersError.missingRequiredField() throw FlatBuffersError.missingRequiredField()
} }
self = .subheader(try RichText(flatBuffersObject: value.text)) 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: case .instantpageblockParagraph:
guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Paragraph.self) else { guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Paragraph.self) else {
throw FlatBuffersError.missingRequiredField() throw FlatBuffersError.missingRequiredField()
@ -554,7 +581,7 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable {
guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Preformatted.self) else { guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Preformatted.self) else {
throw FlatBuffersError.missingRequiredField() throw FlatBuffersError.missingRequiredField()
} }
self = .preformatted(try RichText(flatBuffersObject: value.text)) self = .preformatted(text: try RichText(flatBuffersObject: value.text), language: value.language)
case .instantpageblockFooter: case .instantpageblockFooter:
guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Footer.self) else { guard let value = flatBuffersObject.value(type: TelegramCore_InstantPageBlock_Footer.self) else {
throw FlatBuffersError.missingRequiredField() throw FlatBuffersError.missingRequiredField()
@ -698,17 +725,28 @@ public indirect enum InstantPageBlock: PostboxCoding, Equatable {
let start = TelegramCore_InstantPageBlock_Subheader.startInstantPageBlock_Subheader(&builder) let start = TelegramCore_InstantPageBlock_Subheader.startInstantPageBlock_Subheader(&builder)
TelegramCore_InstantPageBlock_Subheader.add(text: textOffset, &builder) TelegramCore_InstantPageBlock_Subheader.add(text: textOffset, &builder)
offset = TelegramCore_InstantPageBlock_Subheader.endInstantPageBlock_Subheader(&builder, start: start) 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): case let .paragraph(text):
valueType = .instantpageblockParagraph valueType = .instantpageblockParagraph
let textOffset = text.encodeToFlatBuffers(builder: &builder) let textOffset = text.encodeToFlatBuffers(builder: &builder)
let start = TelegramCore_InstantPageBlock_Paragraph.startInstantPageBlock_Paragraph(&builder) let start = TelegramCore_InstantPageBlock_Paragraph.startInstantPageBlock_Paragraph(&builder)
TelegramCore_InstantPageBlock_Paragraph.add(text: textOffset, &builder) TelegramCore_InstantPageBlock_Paragraph.add(text: textOffset, &builder)
offset = TelegramCore_InstantPageBlock_Paragraph.endInstantPageBlock_Paragraph(&builder, start: start) offset = TelegramCore_InstantPageBlock_Paragraph.endInstantPageBlock_Paragraph(&builder, start: start)
case let .preformatted(text): case let .preformatted(text, language):
valueType = .instantpageblockPreformatted valueType = .instantpageblockPreformatted
let textOffset = text.encodeToFlatBuffers(builder: &builder) let textOffset = text.encodeToFlatBuffers(builder: &builder)
let languageOffset = language.flatMap { builder.create(string: $0) }
let start = TelegramCore_InstantPageBlock_Preformatted.startInstantPageBlock_Preformatted(&builder) let start = TelegramCore_InstantPageBlock_Preformatted.startInstantPageBlock_Preformatted(&builder)
TelegramCore_InstantPageBlock_Preformatted.add(text: textOffset, &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) offset = TelegramCore_InstantPageBlock_Preformatted.endInstantPageBlock_Preformatted(&builder, start: start)
case let .footer(text): case let .footer(text):
valueType = .instantpageblockFooter valueType = .instantpageblockFooter

View file

@ -32,6 +32,7 @@ public struct TelegramChatBannedRightsFlags: OptionSet, Hashable {
public static let banSendVoice = TelegramChatBannedRightsFlags(rawValue: 1 << 23) public static let banSendVoice = TelegramChatBannedRightsFlags(rawValue: 1 << 23)
public static let banSendFiles = TelegramChatBannedRightsFlags(rawValue: 1 << 24) public static let banSendFiles = TelegramChatBannedRightsFlags(rawValue: 1 << 24)
public static let banSendText = TelegramChatBannedRightsFlags(rawValue: 1 << 25) public static let banSendText = TelegramChatBannedRightsFlags(rawValue: 1 << 25)
public static let banSendReactions = TelegramChatBannedRightsFlags(rawValue: 1 << 27)
public static let banEditRank = TelegramChatBannedRightsFlags(rawValue: 1 << 26) public static let banEditRank = TelegramChatBannedRightsFlags(rawValue: 1 << 26)
} }

View file

@ -76,6 +76,44 @@ func _internal_deleteAllMessagesWithForwardAuthor(transaction: Transaction, medi
} }
} }
func _internal_deleteAllReactionsWithAuthor(account: Account, peerId: PeerId, authorId: PeerId) -> Signal<Never, NoError> {
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<Never, NoError> in
return .complete()
}
}
}
func _internal_deleteReaction(account: Account, messageId: MessageId, authorId: PeerId) -> Signal<Never, NoError> {
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<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Never, NoError> in
if let updates {
account.stateManager.addUpdates(updates)
}
return .complete()
}
}
}
func _internal_clearHistory(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, threadId: Int64?, namespaces: MessageIdNamespaces) { func _internal_clearHistory(transaction: Transaction, mediaBox: MediaBox, peerId: PeerId, threadId: Int64?, namespaces: MessageIdNamespaces) {
if peerId.namespace == Namespaces.Peer.SecretChat { if peerId.namespace == Namespaces.Peer.SecretChat {
var resourceIds: [MediaResourceId] = [] var resourceIds: [MediaResourceId] = []

View file

@ -169,6 +169,14 @@ public extension TelegramEngine {
} }
|> ignoreValues |> ignoreValues
} }
public func deleteAllReactionsWithAuthor(peerId: EnginePeer.Id, authorId: EnginePeer.Id) -> Signal<Never, NoError> {
return _internal_deleteAllReactionsWithAuthor(account: self.account, peerId: peerId, authorId: authorId)
}
public func deleteReaction(peerId: EnginePeer.Id, messageId: EngineMessage.Id, authorId: EnginePeer.Id) -> Signal<Never, NoError> {
return _internal_deleteReaction(account: self.account, messageId: messageId, authorId: authorId)
}
public func clearCallHistory(forEveryone: Bool) -> Signal<Never, ClearCallHistoryError> { public func clearCallHistory(forEveryone: Bool) -> Signal<Never, ClearCallHistoryError> {
return _internal_clearCallHistory(account: self.account, forEveryone: forEveryone) return _internal_clearCallHistory(account: self.account, forEveryone: forEveryone)

View file

@ -29,6 +29,7 @@ struct MediaRight: OptionSet, Hashable {
static let videoMessages = MediaRight(rawValue: 1 << 6) static let videoMessages = MediaRight(rawValue: 1 << 6)
static let links = MediaRight(rawValue: 1 << 7) static let links = MediaRight(rawValue: 1 << 7)
static let polls = MediaRight(rawValue: 1 << 8) static let polls = MediaRight(rawValue: 1 << 8)
static let reactions = MediaRight(rawValue: 1 << 9)
} }
extension MediaRight { extension MediaRight {
@ -75,7 +76,8 @@ private func rightsFromBannedRights(_ rights: TelegramChatBannedRightsFlags) ->
.voiceMessages, .voiceMessages,
.videoMessages, .videoMessages,
.links, .links,
.polls .polls,
.reactions
] ]
if rights.contains(.banSendText) { if rights.contains(.banSendText) {
@ -168,6 +170,9 @@ private func rightFlagsFromRights(participantRights: ParticipantRight, mediaRigh
if !mediaRights.contains(.polls) { if !mediaRights.contains(.polls) {
result.insert(.banSendPolls) result.insert(.banSendPolls)
} }
if !mediaRights.contains(.reactions) {
result.insert(.banSendReactions)
}
return result return result
} }
@ -181,7 +186,8 @@ private let allMediaRightItems: [MediaRight] = [
.voiceMessages, .voiceMessages,
.videoMessages, .videoMessages,
.links, .links,
.polls .polls,
.reactions
] ]
private enum AdminUserActionOptionSection { private enum AdminUserActionOptionSection {
@ -821,6 +827,8 @@ private final class AdminUserActionsContentComponent: Component {
mediaItemTitle = component.strings.Channel_BanUser_PermissionEmbedLinks mediaItemTitle = component.strings.Channel_BanUser_PermissionEmbedLinks
case .polls: case .polls:
mediaItemTitle = component.strings.Channel_BanUser_PermissionSendPolls mediaItemTitle = component.strings.Channel_BanUser_PermissionSendPolls
case .reactions:
mediaItemTitle = component.strings.Channel_BanUser_PermissionSendReactions
default: default:
continue mediaRightsLoop continue mediaRightsLoop
} }

View file

@ -2619,14 +2619,14 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
switch authorRank { switch authorRank {
case let .creator(rank): case let .creator(rank):
if let rank, !rank.isEmpty { if let rank, !rank.isEmpty {
string = rank.trimmingEmojis string = rank
} else { } else {
string = item.presentationData.strings.Conversation_Owner string = item.presentationData.strings.Conversation_Owner
} }
rankBadgeColor = UIColor(rgb: 0x956ac8) rankBadgeColor = UIColor(rgb: 0x956ac8)
case let .admin(rank): case let .admin(rank):
if let rank, !rank.isEmpty { if let rank, !rank.isEmpty {
string = rank.trimmingEmojis string = rank
} else { } else {
string = item.presentationData.strings.Conversation_Admin string = item.presentationData.strings.Conversation_Admin
} }
@ -2637,7 +2637,7 @@ public class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewI
string = item.presentationData.strings.Chat_TagPlaceholder string = item.presentationData.strings.Chat_TagPlaceholder
defaultRankColor = defaultRankColor.withMultipliedAlpha(0.5) defaultRankColor = defaultRankColor.withMultipliedAlpha(0.5)
} else { } else {
string = rank.trimmingEmojis string = rank
} }
} else { } else {
string = "" string = ""

View file

@ -2634,7 +2634,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
size: CGSize(width: baseWidth, height: panelHeight) 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 { if animateTimeSlideIn {
var previousAudioRecordingTimeFrame = audioRecordingTimeFrame var previousAudioRecordingTimeFrame = audioRecordingTimeFrame

View file

@ -260,7 +260,7 @@ private final class ChatParticipantRightsContent: CombinedComponent {
state.updated(transition: .easeInOut(duration: 0.2)) state.updated(transition: .easeInOut(duration: 0.2))
}, },
shouldUpdateText: { [weak state] text in shouldUpdateText: { [weak state] text in
if text.containsEmoji { if text.containsGraphicEmoji {
state?.animateError() state?.animateError()
return false return false
} }

View file

@ -39,6 +39,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
let currentRepeatPeriod: Int32? let currentRepeatPeriod: Int32?
let suggestedTime: Int32? let suggestedTime: Int32?
let minimalTime: Int32? let minimalTime: Int32?
let silentPosting: Bool
let externalState: ExternalState let externalState: ExternalState
let dismiss: () -> Void let dismiss: () -> Void
@ -49,6 +50,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
currentRepeatPeriod: Int32?, currentRepeatPeriod: Int32?,
suggestedTime: Int32?, suggestedTime: Int32?,
minimalTime: Int32?, minimalTime: Int32?,
silentPosting: Bool,
externalState: ExternalState, externalState: ExternalState,
dismiss: @escaping () -> Void dismiss: @escaping () -> Void
) { ) {
@ -58,6 +60,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
self.currentRepeatPeriod = currentRepeatPeriod self.currentRepeatPeriod = currentRepeatPeriod
self.suggestedTime = suggestedTime self.suggestedTime = suggestedTime
self.minimalTime = minimalTime self.minimalTime = minimalTime
self.silentPosting = silentPosting
self.externalState = externalState self.externalState = externalState
self.dismiss = dismiss self.dismiss = dismiss
} }
@ -222,6 +225,7 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
self.environment = environment self.environment = environment
if self.component == nil { if self.component == nil {
self.isSilentPosting = component.silentPosting
switch component.mode { switch component.mode {
case .format, .search: case .format, .search:
self.minDate = Date(timeIntervalSince1970: 0.0) self.minDate = Date(timeIntervalSince1970: 0.0)
@ -561,7 +565,8 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
controller.completion( controller.completion(
ChatScheduleTimeScreen.Result( ChatScheduleTimeScreen.Result(
time: Int32(self.date?.timeIntervalSince1970 ?? 0), time: Int32(self.date?.timeIntervalSince1970 ?? 0),
repeatPeriod: self.repeatPeriod repeatPeriod: self.repeatPeriod,
silentPosting: self.isSilentPosting
) )
) )
component.dismiss() component.dismiss()
@ -609,7 +614,8 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
controller.completion( controller.completion(
ChatScheduleTimeScreen.Result( ChatScheduleTimeScreen.Result(
time: 0, time: 0,
repeatPeriod: nil repeatPeriod: nil,
silentPosting: false
) )
) )
component.dismiss() component.dismiss()
@ -650,7 +656,8 @@ private final class ChatScheduleTimeSheetContentComponent: Component {
controller.completion( controller.completion(
ChatScheduleTimeScreen.Result( ChatScheduleTimeScreen.Result(
time: scheduleWhenOnlineTimestamp, time: scheduleWhenOnlineTimestamp,
repeatPeriod: nil repeatPeriod: nil,
silentPosting: self.isSilentPosting
) )
) )
component.dismiss() component.dismiss()
@ -918,6 +925,7 @@ private final class ChatScheduleTimeScreenComponent: Component {
let currentRepeatPeriod: Int32? let currentRepeatPeriod: Int32?
let suggestedTime: Int32? let suggestedTime: Int32?
let minimalTime: Int32? let minimalTime: Int32?
let silentPosting: Bool
init( init(
context: AccountContext, context: AccountContext,
@ -925,7 +933,8 @@ private final class ChatScheduleTimeScreenComponent: Component {
currentTime: Int32?, currentTime: Int32?,
currentRepeatPeriod: Int32?, currentRepeatPeriod: Int32?,
suggestedTime: Int32?, suggestedTime: Int32?,
minimalTime: Int32? minimalTime: Int32?,
silentPosting: Bool
) { ) {
self.context = context self.context = context
self.mode = mode self.mode = mode
@ -933,6 +942,7 @@ private final class ChatScheduleTimeScreenComponent: Component {
self.currentRepeatPeriod = currentRepeatPeriod self.currentRepeatPeriod = currentRepeatPeriod
self.suggestedTime = suggestedTime self.suggestedTime = suggestedTime
self.minimalTime = minimalTime self.minimalTime = minimalTime
self.silentPosting = silentPosting
} }
static func ==(lhs: ChatScheduleTimeScreenComponent, rhs: ChatScheduleTimeScreenComponent) -> Bool { static func ==(lhs: ChatScheduleTimeScreenComponent, rhs: ChatScheduleTimeScreenComponent) -> Bool {
@ -951,6 +961,9 @@ private final class ChatScheduleTimeScreenComponent: Component {
if lhs.minimalTime != rhs.minimalTime { if lhs.minimalTime != rhs.minimalTime {
return false return false
} }
if lhs.silentPosting != rhs.silentPosting {
return false
}
return true return true
} }
@ -1005,6 +1018,7 @@ private final class ChatScheduleTimeScreenComponent: Component {
currentRepeatPeriod: component.currentRepeatPeriod, currentRepeatPeriod: component.currentRepeatPeriod,
suggestedTime: component.suggestedTime, suggestedTime: component.suggestedTime,
minimalTime: component.minimalTime, minimalTime: component.minimalTime,
silentPosting: component.silentPosting,
externalState: self.contentExternalState, externalState: self.contentExternalState,
dismiss: { [weak self] in dismiss: { [weak self] in
guard let self else { guard let self else {
@ -1080,6 +1094,13 @@ public class ChatScheduleTimeScreen: ViewControllerComponentContainer {
public struct Result { public struct Result {
public let time: Int32 public let time: Int32
public let repeatPeriod: 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 fileprivate let completion: (Result) -> Void
@ -1091,6 +1112,7 @@ public class ChatScheduleTimeScreen: ViewControllerComponentContainer {
currentRepeatPeriod: Int32? = nil, currentRepeatPeriod: Int32? = nil,
suggestedTime: Int32? = nil, suggestedTime: Int32? = nil,
minimalTime: Int32? = nil, minimalTime: Int32? = nil,
silentPosting: Bool = false,
isDark: Bool, isDark: Bool,
completion: @escaping (Result) -> Void completion: @escaping (Result) -> Void
) { ) {
@ -1102,7 +1124,8 @@ public class ChatScheduleTimeScreen: ViewControllerComponentContainer {
currentTime: currentTime, currentTime: currentTime,
currentRepeatPeriod: currentRepeatPeriod, currentRepeatPeriod: currentRepeatPeriod,
suggestedTime: suggestedTime, suggestedTime: suggestedTime,
minimalTime: minimalTime minimalTime: minimalTime,
silentPosting: silentPosting
), navigationBarAppearance: .none, theme: isDark ? .dark : .default) ), navigationBarAppearance: .none, theme: isDark ? .dark : .default)
self.statusBar.statusBarStyle = .Ignore self.statusBar.statusBarStyle = .Ignore

View file

@ -1407,6 +1407,7 @@ public final class EmojiPagerContentComponent: Component {
private var vibrancyClippingView: UIView private var vibrancyClippingView: UIView
private var vibrancyEffectView: UIView? private var vibrancyEffectView: UIView?
public private(set) var mirrorContentClippingView: UIView? public private(set) var mirrorContentClippingView: UIView?
private let mirrorScrollViewClippingView: UIView
private let mirrorContentScrollView: UIView private let mirrorContentScrollView: UIView
private var warpView: WarpView? private var warpView: WarpView?
private var mirrorContentWarpView: WarpView? private var mirrorContentWarpView: WarpView?
@ -1459,6 +1460,9 @@ public final class EmojiPagerContentComponent: Component {
private var longTapRecognizer: UILongPressGestureRecognizer? private var longTapRecognizer: UILongPressGestureRecognizer?
private func hasSameContentId(_ lhs: ContentId?, _ rhs: ContentId?) -> Bool { private func hasSameContentId(_ lhs: ContentId?, _ rhs: ContentId?) -> Bool {
if let rhs, rhs.version < 2 {
return false
}
return lhs?.id == rhs?.id return lhs?.id == rhs?.id
} }
@ -1480,6 +1484,9 @@ public final class EmojiPagerContentComponent: Component {
self.scrollViewClippingView = UIView() self.scrollViewClippingView = UIView()
self.scrollViewClippingView.clipsToBounds = true self.scrollViewClippingView.clipsToBounds = true
self.mirrorScrollViewClippingView = UIView()
self.mirrorScrollViewClippingView.clipsToBounds = true
self.mirrorContentScrollView = UIView() self.mirrorContentScrollView = UIView()
self.mirrorContentScrollView.layer.anchorPoint = CGPoint() self.mirrorContentScrollView.layer.anchorPoint = CGPoint()
self.mirrorContentScrollView.clipsToBounds = true self.mirrorContentScrollView.clipsToBounds = true
@ -1519,6 +1526,8 @@ public final class EmojiPagerContentComponent: Component {
self.addSubview(self.scrollViewClippingView) self.addSubview(self.scrollViewClippingView)
self.scrollViewClippingView.addSubview(self.scrollView) self.scrollViewClippingView.addSubview(self.scrollView)
self.mirrorScrollViewClippingView.addSubview(self.mirrorContentScrollView)
self.scrollView.addSubview(self.placeholdersContainerView) self.scrollView.addSubview(self.placeholdersContainerView)
let contextGesture = ContextGesture(target: self, action: #selector(self.tapGesture(_:))) 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") 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) { func updateIsWarpEnabled(isEnabled: Bool) {
if isEnabled { if isEnabled {
if self.warpView == nil { if self.warpView == nil {
@ -1671,6 +1690,7 @@ public final class EmojiPagerContentComponent: Component {
let mirrorContentWarpView = WarpView(frame: CGRect()) let mirrorContentWarpView = WarpView(frame: CGRect())
self.mirrorContentWarpView = mirrorContentWarpView self.mirrorContentWarpView = mirrorContentWarpView
self.mirrorScrollViewClippingView.addSubview(mirrorContentWarpView)
mirrorContentWarpView.contentView.addSubview(self.mirrorContentScrollView) mirrorContentWarpView.contentView.addSubview(self.mirrorContentScrollView)
} }
} else { } else {
@ -1683,12 +1703,7 @@ public final class EmojiPagerContentComponent: Component {
if let mirrorContentWarpView = self.mirrorContentWarpView { if let mirrorContentWarpView = self.mirrorContentWarpView {
self.mirrorContentWarpView = nil self.mirrorContentWarpView = nil
if let mirrorContentClippingView = self.mirrorContentClippingView { self.mirrorScrollViewClippingView.addSubview(self.mirrorContentScrollView)
mirrorContentClippingView.addSubview(self.mirrorContentScrollView)
} else if let vibrancyEffectView = self.vibrancyEffectView {
vibrancyEffectView.addSubview(self.mirrorContentScrollView)
}
mirrorContentWarpView.removeFromSuperview() mirrorContentWarpView.removeFromSuperview()
} }
} }
@ -4117,12 +4132,11 @@ public final class EmojiPagerContentComponent: Component {
mirrorContentClippingView = UIView() mirrorContentClippingView = UIView()
mirrorContentClippingView.clipsToBounds = false mirrorContentClippingView.clipsToBounds = false
self.mirrorContentClippingView = mirrorContentClippingView self.mirrorContentClippingView = mirrorContentClippingView
}
if let mirrorContentWarpView = self.mirrorContentWarpView { if self.mirrorScrollViewClippingView.superview !== mirrorContentClippingView {
mirrorContentClippingView.addSubview(mirrorContentWarpView) mirrorContentClippingView.insertSubview(self.mirrorScrollViewClippingView, at: 0)
} else { } else {
mirrorContentClippingView.addSubview(self.mirrorContentScrollView) mirrorContentClippingView.sendSubviewToBack(self.mirrorScrollViewClippingView)
}
} }
let clippingFrame = CGRect(origin: CGPoint(x: 0.0, y: pagerEnvironment.containerInsets.top), size: CGSize(width: backgroundFrame.width, height: backgroundFrame.height)) 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.vibrancyEffectView = vibrancyEffectView
self.backgroundTintView.mask = vibrancyEffectView self.backgroundTintView.mask = vibrancyEffectView
self.vibrancyClippingView.addSubview(self.mirrorContentScrollView)
vibrancyEffectView.addSubview(self.vibrancyClippingView) 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 { 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.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.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 let previousSize = self.scrollView.bounds.size
self.scrollView.bounds = CGRect(origin: self.scrollView.bounds.origin, size: scrollSize) self.scrollView.bounds = CGRect(origin: self.scrollView.bounds.origin, size: scrollSize)
@ -4714,11 +4735,9 @@ public final class EmojiPagerContentComponent: Component {
if self.isSearchActivated { if self.isSearchActivated {
if visibleSearchHeader.superview != self { if visibleSearchHeader.superview != self {
self.addSubview(visibleSearchHeader) self.addSubview(visibleSearchHeader)
if self.mirrorContentClippingView != nil { }
self.mirrorContentClippingView?.addSubview(visibleSearchHeader.tintContainerView) if let mirrorOverlayContainerView = self.mirrorOverlayContainerView, visibleSearchHeader.tintContainerView.superview !== mirrorOverlayContainerView {
} else { mirrorOverlayContainerView.addSubview(visibleSearchHeader.tintContainerView)
self.mirrorContentScrollView.superview?.superview?.addSubview(visibleSearchHeader.tintContainerView)
}
} }
} else { } else {
/*if useOpaqueTheme { /*if useOpaqueTheme {
@ -4779,7 +4798,7 @@ public final class EmojiPagerContentComponent: Component {
self.visibleSearchHeader = visibleSearchHeader self.visibleSearchHeader = visibleSearchHeader
if self.isSearchActivated { if self.isSearchActivated {
self.addSubview(visibleSearchHeader) self.addSubview(visibleSearchHeader)
self.mirrorContentClippingView?.addSubview(visibleSearchHeader.tintContainerView) self.mirrorOverlayContainerView?.addSubview(visibleSearchHeader.tintContainerView)
} else { } else {
self.scrollView.addSubview(visibleSearchHeader) self.scrollView.addSubview(visibleSearchHeader)
self.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView) self.mirrorContentScrollView.addSubview(visibleSearchHeader.tintContainerView)

View file

@ -620,7 +620,7 @@ private final class GiftViewSheetContent: CombinedComponent {
title: presentationData.strings.Gift_Convert_Title, title: presentationData.strings.Gift_Convert_Title,
text: text, text: text,
actions: [ 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 TextAlertAction(type: .defaultAction, title: presentationData.strings.Gift_Convert_Convert, action: { [weak self, weak controller, weak navigationController] in
guard let self else { guard let self else {
return return

View file

@ -11,7 +11,7 @@ import ShareController
import LegacyUI import LegacyUI
import LegacyMediaPickerUI 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 presentationData = context.sharedContext.currentPresentationData.with { $0 }
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme)
legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait) legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait)
@ -45,8 +45,8 @@ public func presentedLegacyCamera(context: AccountContext, peer: Peer?, chatLoca
} }
controller.presentScheduleController = { _, done in controller.presentScheduleController = { _, done in
presentSchedulePicker(true, { time in presentSchedulePicker(true, { time, silentPosting in
done?(time) done?(time, silentPosting)
}) })
} }
controller.presentTimerController = { done in controller.presentTimerController = { done in

View file

@ -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) 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 isSecretChat = peerId.namespace == Namespaces.Peer.SecretChat
let legacyController = InstantVideoController(presentation: .custom, theme: theme) let legacyController = InstantVideoController(presentation: .custom, theme: theme)
@ -166,8 +166,8 @@ public func legacyInstantVideoController(theme: PresentationTheme, forStory: Boo
return node return node
}, canSendSilently: !isSecretChat, canSchedule: hasSchedule, reminder: peerId == context.account.peerId)! }, canSendSilently: !isSecretChat, canSchedule: hasSchedule, reminder: peerId == context.account.peerId)!
controller.presentScheduleController = { done in controller.presentScheduleController = { done in
presentSchedulePicker { time in presentSchedulePicker { time, silentPosting in
done?(time) done?(time, silentPosting)
} }
} }
controller.finishedWithVideo = { [weak legacyController] videoUrl, previewImage, _, duration, dimensions, liveUploadData, adjustments, isSilent, scheduleTimestamp in controller.finishedWithVideo = { [weak legacyController] videoUrl, previewImage, _, duration, dimensions, liveUploadData, adjustments, isSilent, scheduleTimestamp in

View file

@ -1116,7 +1116,7 @@ public func notificationPeerExceptionController(
break break
} }
}), }),
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: { TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {
}) })
], parseMarkdown: true), in: .window(.root)) ], parseMarkdown: true), in: .window(.root))
} }

View file

@ -4533,7 +4533,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
TextAlertAction(type: .destructiveAction, title: actionText, action: { TextAlertAction(type: .destructiveAction, title: actionText, action: {
self?.deletePeerChat(peer: peer._asPeer(), globally: delete) 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)) ], parseMarkdown: true), in: .window(.root))
}) })

View file

@ -5177,7 +5177,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
public func presentDeleteBotPreviewLanguage() { 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: [ 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 TextAlertAction(type: .destructiveAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in
guard let self else { guard let self else {

View file

@ -33,6 +33,7 @@ swift_library(
"//submodules/ActivityIndicator", "//submodules/ActivityIndicator",
"//submodules/DebugSettingsUI", "//submodules/DebugSettingsUI",
"//submodules/ManagedFile", "//submodules/ManagedFile",
"//submodules/ContextUI",
"//submodules/TelegramUI/Components/TelegramUIDeclareEncodables", "//submodules/TelegramUI/Components/TelegramUIDeclareEncodables",
"//submodules/TelegramUI/Components/AnimationCache", "//submodules/TelegramUI/Components/AnimationCache",
"//submodules/TelegramUI/Components/MultiAnimationRenderer", "//submodules/TelegramUI/Components/MultiAnimationRenderer",
@ -41,6 +42,7 @@ swift_library(
"//submodules/TelegramUI/Components/TelegramAccountAuxiliaryMethods", "//submodules/TelegramUI/Components/TelegramAccountAuxiliaryMethods",
"//submodules/TelegramUI/Components/PeerSelectionController", "//submodules/TelegramUI/Components/PeerSelectionController",
"//submodules/TelegramUI/Components/ContextMenuScreen", "//submodules/TelegramUI/Components/ContextMenuScreen",
"//submodules/TelegramUI/Components/ContextControllerImpl",
"//submodules/TelegramUI/Components/NavigationBarImpl", "//submodules/TelegramUI/Components/NavigationBarImpl",
], ],
visibility = [ visibility = [

View file

@ -34,6 +34,8 @@ import TelegramAccountAuxiliaryMethods
import PeerSelectionController import PeerSelectionController
import ContextMenuScreen import ContextMenuScreen
import NavigationBarImpl import NavigationBarImpl
import ContextUI
import ContextControllerImpl
private var installedSharedLogger = false private var installedSharedLogger = false
@ -201,6 +203,18 @@ public class ShareRootControllerImpl {
defaultNavigationBarImpl = { presentationData in defaultNavigationBarImpl = { presentationData in
return NavigationBarImpl(presentationData: presentationData) 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 { deinit {

View file

@ -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 { guard let component = view.component else {
return return
} }
@ -987,6 +987,8 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
replyTo: nil, replyTo: nil,
storyId: focusedStoryId, storyId: focusedStoryId,
content: .contextResult(results, result), content: .contextResult(results, result),
silentPosting: silentPosting,
scheduleTime: scheduleTime,
sendPaidMessageStars: component.slice.additionalPeerData.sendPaidMessageStars sendPaidMessageStars: component.slice.additionalPeerData.sendPaidMessageStars
) |> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in ) |> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in
Queue.mainQueue().after(0.3) { Queue.mainQueue().after(0.3) {
@ -1140,8 +1142,8 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
guard let self, let view else { guard let self, let view else {
return return
} }
self.presentScheduleTimePicker(view: view, peer: peer, completion: { time, repeatPeriod in self.presentScheduleTimePicker(view: view, peer: peer, completion: { time, repeatPeriod, silentPosting in
done(time) done(time, silentPosting)
}) })
}))) })))
} }
@ -2292,8 +2294,8 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
guard let self, let view else { guard let self, let view else {
return return
} }
self.presentScheduleTimePicker(view: view, peer: peer, style: media ? .media : .default, completion: { time, repeatPeriod in self.presentScheduleTimePicker(view: view, peer: peer, style: media ? .media : .default, completion: { time, repeatPeriod, silentPosting in
done(time) done(time, silentPosting)
}) })
} }
controller.presentTimerPicker = { [weak self, weak view] done in controller.presentTimerPicker = { [weak self, weak view] done in
@ -2372,7 +2374,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
if let view, let component = view.component { if let view, let component = view.component {
let theme = component.theme let theme = component.theme
let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }) let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>) = (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) })
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 { if let legacyController = legacyController {
legacyController.dismiss() legacyController.dismiss()
} }
@ -2381,14 +2383,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
} }
legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak view] result in legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak view] result in
if let strongSelf = self, let view { 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 }, enqueueMediaMessages: { [weak view] signals in
if let strongSelf = self, let view { if let strongSelf = self, let view {
if editingMedia { if editingMedia {
strongSelf.editMessageMediaWithLegacySignals(view: view, signals: signals) strongSelf.editMessageMediaWithLegacySignals(view: view, signals: signals)
} else { } 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)) 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 }, presentSchedulePicker: { [weak view] media, done in
if let strongSelf = self, let view { if let strongSelf = self, let view {
strongSelf.presentScheduleTimePicker(view: view, peer: peer, style: media ? .media : .default, completion: { time, repeatPeriod in strongSelf.presentScheduleTimePicker(view: view, peer: peer, style: media ? .media : .default, completion: { time, repeatPeriod, silentPosting in
done(time) done(time, silentPosting)
}) })
} }
}, presentTimerPicker: { [weak view] done in }, presentTimerPicker: { [weak view] done in
@ -2640,7 +2642,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
// component.controller()?.push(controller) // 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()) { if !canSendMessagesToPeer(peer._asPeer()) {
return 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) { 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()) let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots())
|> deliverOnMainQueue).start(next: { [weak self, weak view] configuration in |> deliverOnMainQueue).start(next: { [weak self, weak view] configuration in
if let self { 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 legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak self, weak view] result in
if let self, let view { 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 }, enqueueMediaMessages: { [weak self, weak view] signals in
if let self, let view, !signals.isEmpty { 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) }), activateOnDisplay: activateOnDisplay)
@ -2833,8 +2835,8 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
guard let self, let view else { guard let self, let view else {
return return
} }
self.presentScheduleTimePicker(view: view, peer: peer, style: .media, completion: { time, repeatPeriod in self.presentScheduleTimePicker(view: view, peer: peer, style: .media, completion: { time, repeatPeriod, silentPosting in
done(time) done(time, silentPosting)
}) })
}, presentTimerPicker: { [weak self, weak view] done in }, presentTimerPicker: { [weak self, weak view] done in
guard let self, let view else { guard let self, let view else {
@ -2868,7 +2870,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
style: ChatScheduleTimeControllerStyle = .default, style: ChatScheduleTimeControllerStyle = .default,
selectedTime: Int32? = nil, selectedTime: Int32? = nil,
dismissByTapOutside: Bool = true, dismissByTapOutside: Bool = true,
completion: @escaping (Int32, Int32?) -> Void completion: @escaping (Int32, Int32?, Bool) -> Void
) { ) {
guard let component = view.component else { guard let component = view.component else {
return return
@ -2889,16 +2891,25 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
sendWhenOnlineAvailable = false sendWhenOnlineAvailable = false
} }
let mode: ChatScheduleTimeControllerMode let mode: ChatScheduleTimeScreen.Mode //ChatScheduleTimeControllerMode
if peer.id == component.context.account.peerId { if peer.id == component.context.account.peerId {
mode = .reminders mode = .reminders
} else { } else {
mode = .scheduledMessages(sendWhenOnlineAvailable: sendWhenOnlineAvailable) mode = .scheduledMessages(peerId: peer.id, sendWhenOnlineAvailable: sendWhenOnlineAvailable)
} }
let theme = component.theme let controller = ChatScheduleTimeScreen(
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 context: component.context,
completion(time, nil) 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.endEditing(true)
view.component?.controller()?.present(controller, in: .window(.root)) view.component?.controller()?.present(controller, in: .window(.root))
}) })

View file

@ -441,7 +441,7 @@ private final class VideoMessageCameraScreenComponent: CombinedComponent {
} }
}, error: { [weak self] _ in }, error: { [weak self] _ in
if let self, let controller = self.getController() { 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 allowLiveUpload: Bool
fileprivate var viewOnceAvailable: Bool fileprivate var viewOnceAvailable: Bool
fileprivate let completion: (EnqueueMessage?, Bool?, Int32?) -> Void fileprivate let completion: (EnqueueMessage?, Bool?, Int32?, Int32?) -> Void
private var audioSessionDisposable: Disposable? private var audioSessionDisposable: Disposable?
@ -1794,7 +1794,7 @@ public class VideoMessageCameraScreen: ViewController {
viewOnceAvailable: Bool, viewOnceAvailable: Bool,
inputPanelFrame: (CGRect, Bool), inputPanelFrame: (CGRect, Bool),
chatNode: ASDisplayNode?, chatNode: ASDisplayNode?,
completion: @escaping (EnqueueMessage?, Bool?, Int32?) -> Void completion: @escaping (EnqueueMessage?, Bool?, Int32?, Int32?) -> Void
) { ) {
self.context = context self.context = context
self.updatedPresentationData = updatedPresentationData self.updatedPresentationData = updatedPresentationData
@ -1833,7 +1833,7 @@ public class VideoMessageCameraScreen: ViewController {
fileprivate var didSend = false fileprivate var didSend = false
fileprivate var lastActionTimestamp: Double? fileprivate var lastActionTimestamp: Double?
fileprivate var isSendingImmediately = false 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 { guard !self.didSend else {
return return
} }
@ -1845,7 +1845,7 @@ public class VideoMessageCameraScreen: ViewController {
} }
if case .none = self.cameraState.recording, self.node.results.isEmpty { if case .none = self.cameraState.recording, self.node.results.isEmpty {
self.completion(nil, nil, nil) self.completion(nil, nil, nil, nil)
return return
} }
@ -1859,7 +1859,7 @@ public class VideoMessageCameraScreen: ViewController {
self.waitingForNextResult = true self.waitingForNextResult = true
self.node.stopRecording.invoke(Void()) self.node.stopRecording.invoke(Void())
} else { } else {
self.completion(nil, nil, nil) self.completion(nil, nil, nil, nil)
return return
} }
} }
@ -1889,7 +1889,7 @@ public class VideoMessageCameraScreen: ViewController {
} }
if duration < 1.0 { if duration < 1.0 {
self.completion(nil, nil, nil) self.completion(nil, nil, nil, nil)
return return
} }
@ -1998,7 +1998,7 @@ public class VideoMessageCameraScreen: ViewController {
localGroupingKey: nil, localGroupingKey: nil,
correlationId: nil, correlationId: nil,
bubbleUpEmojiOrStickersets: [] bubbleUpEmojiOrStickersets: []
), silentPosting, scheduleTime) ), silentPosting, scheduleTime, repeatPeriod)
}) })
}) })
} }

View file

@ -35,12 +35,12 @@ extension ChatControllerImpl {
guard let self else { guard let self else {
return return
} }
self.presentScheduleTimePicker(style: .media, completion: { [weak self] time, _ in self.presentScheduleTimePicker(style: .media, completion: { [weak self] result in
guard let self else { guard let self else {
return return
} }
done(time) done(result.time, result.silentPosting)
if self.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { if self.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp {
self.openScheduledMessages() self.openScheduledMessages()
} }
}) })

View file

@ -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 forwardedMessages: [[EnqueueMessage]] = []
var forwardSourcePeerIds = Set<PeerId>() var forwardSourcePeerIds = Set<PeerId>()
@ -1962,7 +1963,7 @@ extension ChatControllerImpl {
} }
self.beginDeleteMessagesWithUndo(messageIds: Set(messages.map({ $0.id })), type: .forEveryone) 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 actionLayout: .vertical
) )

View file

@ -182,7 +182,7 @@ extension ChatControllerImpl {
viewOnceAvailable: viewOnceAvailable, viewOnceAvailable: viewOnceAvailable,
inputPanelFrame: (currentInputPanelFrame, self.chatDisplayNode.inputNode != nil), inputPanelFrame: (currentInputPanelFrame, self.chatDisplayNode.inputNode != nil),
chatNode: self.chatDisplayNode.historyNode, 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 { guard let self, let videoController = self.videoRecorderValue else {
return return
} }
@ -230,14 +230,8 @@ extension ChatControllerImpl {
}, usedCorrelationId ? correlationId : nil) }, usedCorrelationId ? correlationId : nil)
let messages = [message] let messages = [message]
let transformedMessages: [EnqueueMessage] let effectiveSilentPosting = silentPosting ?? self.presentationInterfaceState.interfaceState.silentPosting
if let silentPosting { let transformedMessages = self.transformEnqueueMessages(messages, silentPosting: effectiveSilentPosting, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod)
transformedMessages = self.transformEnqueueMessages(messages, silentPosting: silentPosting)
} else if let scheduleTime {
transformedMessages = self.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime)
} else {
transformedMessages = self.transformEnqueueMessages(messages)
}
self.sendMessages(transformedMessages) 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 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] let effectiveSilentPosting = silentPosting ?? self.presentationInterfaceState.interfaceState.silentPosting
if let silentPosting = silentPosting { let transformedMessages = self.transformEnqueueMessages(messages, silentPosting: effectiveSilentPosting, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod, postpone: postpone)
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)
}
guard let peerId = self.chatLocation.peerId else { guard let peerId = self.chatLocation.peerId else {
return return
@ -783,7 +771,7 @@ extension ChatControllerImpl {
guard let videoRecorderValue = self.videoRecorderValue else { guard let videoRecorderValue = self.videoRecorderValue else {
return return
} }
videoRecorderValue.sendVideoRecording(silentPosting: silentPosting, scheduleTime: scheduleTime, messageEffect: messageEffect) videoRecorderValue.sendVideoRecording(silentPosting: silentPosting, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod, messageEffect: messageEffect)
} }
} }
} }

View file

@ -94,7 +94,7 @@ func updateChatPresentationInterfaceStateImpl(
guard let selfController, value else { guard let selfController, value else {
return 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 }), TextAlertAction(type: .defaultAction, title: selfController.presentationData.strings.Common_OK, action: { [weak selfController] in
guard let selfController else { guard let selfController else {
return return
@ -151,7 +151,7 @@ func updateChatPresentationInterfaceStateImpl(
case .generic: case .generic:
break break
case let .inlineBotLocationRequest(peerId): 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 { guard let selfController else {
return return
} }

View file

@ -2387,9 +2387,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
}, shouldAnimateMessageTransition ? correlationId : nil) }, shouldAnimateMessageTransition ? correlationId : nil)
strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime, repeatPeriod in strongSelf.presentScheduleTimePicker(completion: { [weak self] result in
if let strongSelf = self { 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) strongSelf.sendMessages(transformedMessages)
} }
}) })
@ -2557,9 +2557,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
messages = strongSelf.transformEnqueueMessages(messages, silentPosting: true) messages = strongSelf.transformEnqueueMessages(messages, silentPosting: true)
strongSelf.sendMessages(messages) strongSelf.sendMessages(messages)
} else if schedule { } else if schedule {
strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime, repeatPeriod in strongSelf.presentScheduleTimePicker(completion: { [weak self] result in
if let strongSelf = self { 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) strongSelf.sendMessages(transformedMessages)
} }
}) })
@ -4031,15 +4031,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard !self.presentAccountFrozenInfoIfNeeded(delay: true) else { guard !self.presentAccountFrozenInfoIfNeeded(delay: true) else {
return return
} }
self.presentScheduleTimePicker(completion: { [weak self] time, repeatPeriod in self.presentScheduleTimePicker(completion: { [weak self] result in
if let strongSelf = self { if let strongSelf = self {
if let _ = strongSelf.presentationInterfaceState.interfaceState.mediaDraftState { 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) return ChatSendMessageEffect(id: $0.id)
}) })
} else { } else {
let silentPosting = strongSelf.presentationInterfaceState.interfaceState.silentPosting strongSelf.chatDisplayNode.sendCurrentMessage(silentPosting: result.silentPosting, scheduleTime: result.time, repeatPeriod: result.repeatPeriod, messageEffect: (params?.effect).flatMap {
strongSelf.chatDisplayNode.sendCurrentMessage(silentPosting: silentPosting, scheduleTime: time, repeatPeriod: repeatPeriod, messageEffect: (params?.effect).flatMap {
return ChatSendMessageEffect(id: $0.id) return ChatSendMessageEffect(id: $0.id)
}) { [weak self] in }) { [weak self] in
if let strongSelf = self { 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: ""))) } $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() strongSelf.openScheduledMessages()
} }
} }
@ -8679,9 +8678,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}) })
} }
} else { } 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 { 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) { if !canSendMessagesToChat(self.presentationInterfaceState) {
return return
} }
@ -8955,7 +8954,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
isScheduledMessages = true isScheduledMessages = true
} }
let sendMessage: (Int32?) -> Void = { [weak self] scheduleTime in let sendMessage: (Int32?, Bool) -> Void = { [weak self] scheduleTime, silentPosting in
guard let self else { guard let self else {
return return
} }
@ -8991,12 +8990,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
} }
} }
if isScheduledMessages { if let scheduleTime {
self.presentScheduleTimePicker(style: .default, dismissByTapOutside: false, completion: { time, repeatPeriod in sendMessage(scheduleTime, silentPosting)
sendMessage(time) } else if isScheduledMessages {
self.presentScheduleTimePicker(style: .default, dismissByTapOutside: false, completion: { result in
sendMessage(result.time, result.silentPosting)
}) })
} else { } 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) { 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 { guard let peerId = self.chatLocation.peerId else {
return return
} }
@ -10279,9 +10286,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
currentTime: selectedTime, currentTime: selectedTime,
currentRepeatPeriod: selectedRepeatPeriod, currentRepeatPeriod: selectedRepeatPeriod,
minimalTime: strongSelf.presentationInterfaceState.slowmodeState?.timeout, minimalTime: strongSelf.presentationInterfaceState.slowmodeState?.timeout,
silentPosting: strongSelf.presentationInterfaceState.interfaceState.silentPosting,
isDark: style == .media, isDark: style == .media,
completion: { result in completion: { result in
completion(result.time, result.repeatPeriod) completion(result)
} }
) )
strongSelf.chatDisplayNode.dismissInput() strongSelf.chatDisplayNode.dismissInput()

View file

@ -21,7 +21,7 @@ fileprivate struct InitialBannedRights {
} }
extension ChatControllerImpl { extension ChatControllerImpl {
fileprivate func applyAdminUserActionsResult(messageIds: Set<MessageId>, result: AdminUserActionsSheet.ChatResult, initialUserBannedRights: [EnginePeer.Id: InitialBannedRights]) { fileprivate func applyAdminUserActionsResult(messageIds: Set<MessageId>, reactionPeerId: EnginePeer.Id? = nil, result: AdminUserActionsSheet.ChatResult, initialUserBannedRights: [EnginePeer.Id: InitialBannedRights]) {
guard let messagesPeerId = self.chatLocation.peerId else { guard let messagesPeerId = self.chatLocation.peerId else {
return 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 { if !result.deleteAllFromPeers.isEmpty {
title = self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTitleMultiple title = self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTitleMultiple
} }
@ -74,6 +79,10 @@ extension ChatControllerImpl {
let _ = self.context.engine.messages.clearAuthorHistory(peerId: messagesPeerId, memberId: authorId).startStandalone() 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 { for authorId in result.reportSpamPeers {
let _ = self.context.engine.peers.reportPeer(peerId: authorId, reason: .spam, message: "").startStandalone() let _ = self.context.engine.peers.reportPeer(peerId: authorId, reason: .spam, message: "").startStandalone()
} }
@ -88,9 +97,17 @@ extension ChatControllerImpl {
} }
if text.isEmpty { if text.isEmpty {
text = messageIds.count == 1 ? self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextSingle : self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextMultiple if let _ = reactionPeerId {
if !result.deleteAllFromPeers.isEmpty { 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 text = self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextMultiple
} else if !result.deleteAllReactionsFromPeers.isEmpty {
text = self.presentationData.strings.Chat_AdminAction_ToastReactionsDeletedTextMultiple
} }
title = nil title = nil
} }
@ -332,7 +349,7 @@ extension ChatControllerImpl {
guard let self else { guard let self else {
return return
} }
self.applyAdminUserActionsResult(messageIds: messageIds, result: result, initialUserBannedRights: initialUserBannedRights) self.applyAdminUserActionsResult(messageIds: messageIds, reactionPeerId: authorPeer.id, result: result, initialUserBannedRights: initialUserBannedRights)
}) })
} else { } else {
mode = .chat( mode = .chat(
@ -428,7 +445,7 @@ extension ChatControllerImpl {
} }
self.beginDeleteMessagesWithUndo(messageIds: messageIds, type: .forEveryone) 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, actionLayout: .vertical,
parseMarkdown: true parseMarkdown: true
@ -490,7 +507,7 @@ extension ChatControllerImpl {
} }
self.beginDeleteMessagesWithUndo(messageIds: messageIds, type: .forEveryone) 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, actionLayout: .vertical,
parseMarkdown: true parseMarkdown: true
@ -568,7 +585,7 @@ extension ChatControllerImpl {
let dateString = stringForDate(timestamp: giveaway.untilDate, timeZone: .current, strings: strongSelf.presentationData.strings) 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: { 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() 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)) })], parseMarkdown: true), in: .window(.root))
} }
f(.default) f(.default)

View file

@ -317,14 +317,14 @@ extension ChatControllerImpl {
let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: true) let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: true)
commit(transformedMessages) commit(transformedMessages)
case .schedule: case .schedule:
strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime, repeatPeriod in strongSelf.presentScheduleTimePicker(completion: { [weak self] timeResult in
if let strongSelf = self { 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) commit(transformedMessages)
} }
}) })
case .whenOnline: 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) commit(transformedMessages)
} }
} }

View file

@ -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)) 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 contactsController.presentScheduleTimePicker = { [weak self] completion in
if let strongSelf = self { if let strongSelf = self {
strongSelf.presentScheduleTimePicker(completion: completion) strongSelf.presentScheduleTimePicker(completion: { result in
completion(result.time, result.repeatPeriod, result.silentPosting)
})
} }
} }
contactsController.navigationPresentation = .modal contactsController.navigationPresentation = .modal
@ -1057,10 +1059,10 @@ extension ChatControllerImpl {
} }
}, presentSchedulePicker: { [weak self] _, done in }, presentSchedulePicker: { [weak self] _, done in
if let strongSelf = self { 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 { if let strongSelf = self {
done(time) done(result.time, result.silentPosting)
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp {
strongSelf.openScheduledMessages() strongSelf.openScheduledMessages()
} }
} }
@ -1114,10 +1116,10 @@ extension ChatControllerImpl {
})], actionLayout: .vertical), in: .window(.root)) })], actionLayout: .vertical), in: .window(.root))
}, presentSchedulePicker: { [weak self] _, done in }, presentSchedulePicker: { [weak self] _, done in
if let strongSelf = self { 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 { if let strongSelf = self {
done(time) done(result.time, result.silentPosting)
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp {
strongSelf.openScheduledMessages() strongSelf.openScheduledMessages()
} }
} }
@ -1367,10 +1369,10 @@ extension ChatControllerImpl {
} }
controller.presentSchedulePicker = { [weak self] media, done in controller.presentSchedulePicker = { [weak self] media, done in
if let strongSelf = self { 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 { if let strongSelf = self {
done(time) done(result.time, result.silentPosting)
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp {
strongSelf.openScheduledMessages() 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 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 { 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 { if let legacyController = legacyController {
legacyController.dismiss() legacyController.dismiss()
} }
legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { result in legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { result in
if let strongSelf = self { if let strongSelf = self {
strongSelf.enqueueChatContextResult(results, result, hideVia: true) strongSelf.enqueueChatContextResult(results, result, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime)
} }
}, enqueueMediaMessages: { signals in }, enqueueMediaMessages: { signals in
if let strongSelf = self { if let strongSelf = self {
if editingMedia { if editingMedia {
strongSelf.editMessageMediaWithLegacySignals(signals) strongSelf.editMessageMediaWithLegacySignals(signals)
} else { } 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)) 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 }, presentSchedulePicker: { [weak self] media, done in
if let strongSelf = self { 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 { if let strongSelf = self {
done(time) done(result.time, result.silentPosting)
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp {
strongSelf.openScheduledMessages() strongSelf.openScheduledMessages()
} }
} }
@ -1578,18 +1580,18 @@ extension ChatControllerImpl {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots()) let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots())
|> deliverOnMainQueue).startStandalone(next: { [weak self] configuration in |> deliverOnMainQueue).startStandalone(next: { [weak self] configuration in
if let strongSelf = self { 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) self?.attachmentController?.dismiss(animated: true, completion: nil)
legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak self] result in legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak self] result in
if let strongSelf = self { 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 }, enqueueMediaMessages: { [weak self] signals in
if let strongSelf = self, !signals.isEmpty { if let strongSelf = self, !signals.isEmpty {
if editingMessage { if editingMessage {
strongSelf.editMessageMediaWithLegacySignals(signals) strongSelf.editMessageMediaWithLegacySignals(signals)
} else { } 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 }, presentSchedulePicker: { [weak self] _, done in
if let strongSelf = self { 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 { if let strongSelf = self {
done(time) done(result.time, result.silentPosting)
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp { if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp {
strongSelf.openScheduledMessages() strongSelf.openScheduledMessages()
} }
} }

View file

@ -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? { func textInputContextPanel(context: AccountContext, chatPresentationInterfaceState: ChatPresentationInterfaceState, controllerInteraction: ChatControllerInteraction?, interfaceInteraction: ChatPanelInterfaceInteraction?, currentPanel: ChatInputContextPanelNode?) -> ChatInputContextPanelNode? {
guard let controllerInteraction else { guard let controllerInteraction else {
return nil return nil
@ -45,15 +56,8 @@ func textInputContextPanel(context: AccountContext, chatPresentationInterfaceSta
}).first else { }).first else {
return nil return nil
} }
var hasBannedInlineContent = false if hasBannedInlineContent(chatPresentationInterfaceState: chatPresentationInterfaceState) {
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 {
switch inputQueryResult { switch inputQueryResult {
case .stickers, .contextRequestResult: case .stickers, .contextRequestResult:
if let currentPanel = currentPanel as? DisabledContextResultsChatInputContextPanelNode { if let currentPanel = currentPanel as? DisabledContextResultsChatInputContextPanelNode {
@ -183,15 +187,8 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa
}).first else { }).first else {
return nil return nil
} }
var hasBannedInlineContent = false if hasBannedInlineContent(chatPresentationInterfaceState: chatPresentationInterfaceState) {
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 {
switch inputQueryResult { switch inputQueryResult {
case .stickers, .contextRequestResult: case .stickers, .contextRequestResult:
if let currentPanel = currentPanel as? DisabledContextResultsChatInputContextPanelNode { if let currentPanel = currentPanel as? DisabledContextResultsChatInputContextPanelNode {
@ -305,4 +302,3 @@ func chatOverlayContextPanelForChatPresentationIntefaceState(_ chatPresentationI
return nil return nil
} }

View file

@ -70,7 +70,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
private let isPeerEnabled: (ContactListPeer) -> Bool private let isPeerEnabled: (ContactListPeer) -> Bool
var dismissed: (() -> Void)? 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 createActionDisposable = MetaDisposable()
private let confirmationDisposable = MetaDisposable() private let confirmationDisposable = MetaDisposable()
@ -646,8 +646,8 @@ final class ContactsPickerContext: AttachmentMediaPickerContext {
} }
func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) { func schedule(parameters: ChatSendMessageActionSheetController.SendParameters?) {
self.controller?.presentScheduleTimePicker ({ time, repeatPeriod in self.controller?.presentScheduleTimePicker ({ time, repeatPeriod, silentPosting in
self.controller?.contactsNode.requestMultipleAction?(false, time, parameters) self.controller?.contactsNode.requestMultipleAction?(silentPosting, time, parameters)
}) })
} }

View file

@ -19,8 +19,12 @@ import PeerInfoUI
import MapResourceToAvatarSizes import MapResourceToAvatarSizes
import LegacyMediaPickerUI import LegacyMediaPickerUI
import TextFormat import TextFormat
import MediaEditor
import MediaEditorScreen
import CameraScreen
import AvatarEditorScreen import AvatarEditorScreen
import OldChannelsController import OldChannelsController
import Photos
import AVFoundation import AVFoundation
private struct CreateChannelArguments { private struct CreateChannelArguments {
@ -363,10 +367,29 @@ public func createChannelController(context: AccountContext, mode: CreateChannel
let actionsDisposable = DisposableSet() let actionsDisposable = DisposableSet()
let currentAvatarMixin = Atomic<TGMediaAvatarMenuMixin?>(value: nil) var avatarPickerHolder: Any?
var pendingAvatar: CreatePendingPeerAvatar?
let uploadedAvatar = Promise<UploadedPeerPhotoData>() let applyPendingAvatar: (CreatePendingPeerAvatar) -> Void = { avatar in
var uploadedVideoAvatar: (Promise<UploadedPeerPhotoData?>, Double?)? = nil 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() let checkAddressNameDisposable = MetaDisposable()
actionsDisposable.add(checkAddressNameDisposable) actionsDisposable.add(checkAddressNameDisposable)
@ -434,11 +457,8 @@ public func createChannelController(context: AccountContext, mode: CreateChannel
} }
} }
}).start(next: { peerId in }).start(next: { peerId in
let updatingAvatar = stateValue.with { if let pendingAvatar {
return $0.avatar let _ = context.engine.peers.updatePeerPhoto(peerId: peerId, photo: pendingAvatar.uploadedPhoto, video: pendingAvatar.uploadedVideo, videoStartTimestamp: pendingAvatar.videoStartTimestamp, markup: pendingAvatar.markup, mapResourceToAvatarSizes: { resource, representations in
}
if let _ = updatingAvatar {
let _ = context.engine.peers.updatePeerPhoto(peerId: peerId, photo: uploadedAvatar.get(), video: uploadedVideoAvatar?.0.get(), videoStartTimestamp: uploadedVideoAvatar?.1, mapResourceToAvatarSizes: { resource, representations in
return mapResourceToAvatarSizes(engine: context.engine, resource: resource, representations: representations) return mapResourceToAvatarSizes(engine: context.engine, resource: resource, representations: representations)
}).start() }).start()
} }
@ -474,216 +494,127 @@ public func createChannelController(context: AccountContext, mode: CreateChannel
}, changeProfilePhoto: { }, changeProfilePhoto: {
endEditingImpl?() endEditingImpl?()
let title = stateValue.with { state -> String in let keyboardInputData = Promise<AvatarKeyboardInputData>()
return state.editingName.composedTitle keyboardInputData.set(AvatarEditorScreen.inputData(context: context, isGroup: true))
}
let _ = (context.engine.data.get( var dismissPickerImpl: (() -> Void)?
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), let (mainController, pickerHolder) = context.sharedContext.makeAvatarMediaPickerScreen(context: context, getSourceRect: { return nil }, canDelete: pendingAvatar != nil, performDelete: {
TelegramEngine.EngineData.Item.Configuration.SearchBots() clearPendingAvatar()
) }, completion: { result, transitionView, transitionRect, transitionImage, fromCamera, _, cancelled in
|> deliverOnMainQueue).start(next: { peer, searchBotsConfiguration in avatarPickerHolder = nil
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) let applyPhoto: (UIImage) -> Void = { image in
legacyController.statusBar.statusBarStyle = .Ignore if let avatar = CreatePeerAvatarSetup.photo(context: context, image: image) {
applyPendingAvatar(avatar)
let emptyController = LegacyEmptyController(context: legacyController.context)! }
let navigationController = makeLegacyNavigationController(rootController: emptyController) }
navigationController.setNavigationBarHidden(true, animated: false) let applyVideo: (UIImage, MediaEditorScreenImpl.MediaResult.VideoResult?, MediaEditorValues?, UploadPeerPhotoMarkup?) -> Void = { image, video, values, markup in
navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) if let avatar = CreatePeerAvatarSetup.video(context: context, image: image, video: video, values: values, markup: markup, didCompleteLoadingPreview: { avatar in
updatePendingAvatarIfCurrent(avatar)
legacyController.bind(controller: navigationController) }) {
applyPendingAvatar(avatar)
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 completedChannelVideoImpl: (UIImage, Any?, TGVideoEditAdjustments?) -> Void = { image, asset, adjustments in let subject: Signal<MediaEditorScreenImpl.Subject?, NoError>
if let data = image.jpegData(compressionQuality: 0.6) { if let asset = result as? PHAsset {
let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) subject = .single(.asset(asset))
context.engine.resources.storeResourceData(id: EngineMediaResource.Id(photoResource.id), data: data) } else if let image = result as? UIImage {
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight, fromCamera: false))
updateState { state in } else if let result = result as? Signal<CameraScreenImpl.Result, NoError> {
var state = state subject = result
state.avatar = .image(representation, true) |> map { value -> MediaEditorScreenImpl.Subject? in
return state 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<TelegramMediaResource?, UploadPeerPhotoError> { 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<UploadedPeerPhotoData?>()
promise.set(signal
|> `catch` { _ -> Signal<TelegramMediaResource?, NoError> in
return .single(nil)
}
|> mapToSignal { resource -> Signal<UploadedPeerPhotoData?, NoError> 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<AvatarKeyboardInputData>() let editorController = MediaEditorScreenImpl(
keyboardInputData.set(AvatarEditorScreen.inputData(context: context, isGroup: true)) context: context,
mode: .avatarEditor,
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)! subject: subject,
mixin.stickersContext = LegacyPaintStickersContext(context: context) transitionIn: fromCamera ? .camera : transitionView.flatMap({ .gallery(
let _ = currentAvatarMixin.swap(mixin) MediaEditorScreenImpl.TransitionIn.GalleryTransitionIn(
mixin.requestSearchController = { assetsController in sourceView: $0,
let controller = WebSearchController(context: context, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: title, completion: { result in sourceRect: transitionRect,
assetsController?.dismiss() sourceImage: transitionImage
completedChannelPhotoImpl(result) )
})) ) }),
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) transitionOut: { finished, _ in
} if !finished, let transitionView {
// mixin.requestAvatarEditor = { imageCompletion, videoCompletion in return MediaEditorScreenImpl.TransitionOut(
// guard let imageCompletion, let videoCompletion else { destinationView: transitionView,
// return destinationRect: transitionView.bounds,
// } destinationCornerRadius: 0.0
// 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
} }
uploadedAvatar.set(.never()) return nil
} },
} completion: { results, commit in
mixin.didDismiss = { [weak legacyController] in guard let result = results.first else {
let _ = currentAvatarMixin.swap(nil) return
legacyController?.dismiss() }
} switch result.media {
let menuController = mixin.present() case let .image(image, _):
if let menuController = menuController { applyPhoto(image)
menuController.customRemoveFromParentViewController = { [weak legacyController] in commit({})
legacyController?.dismiss() 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: { }, focusOnDescription: {
focusOnDescriptionImpl?() focusOnDescriptionImpl?()
}, updatePublicLinkText: { text in }, updatePublicLinkText: { text in
@ -741,6 +672,8 @@ public func createChannelController(context: AccountContext, mode: CreateChannel
return (controllerState, (listState, arguments)) return (controllerState, (listState, arguments))
} |> afterDisposed { } |> afterDisposed {
actionsDisposable.dispose() actionsDisposable.dispose()
let _ = avatarPickerHolder
} }
let controller = ItemListController(context: context, state: signal) let controller = ItemListController(context: context, state: signal)

View file

@ -34,6 +34,10 @@ import TextFormat
import AvatarEditorScreen import AvatarEditorScreen
import SendInviteLinkScreen import SendInviteLinkScreen
import OldChannelsController import OldChannelsController
import MediaEditor
import MediaEditorScreen
import CameraScreen
import Photos
import AVFoundation import AVFoundation
private struct CreateGroupArguments { private struct CreateGroupArguments {
@ -563,10 +567,29 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
let checkAddressNameDisposable = MetaDisposable() let checkAddressNameDisposable = MetaDisposable()
actionsDisposable.add(checkAddressNameDisposable) actionsDisposable.add(checkAddressNameDisposable)
let currentAvatarMixin = Atomic<TGMediaAvatarMenuMixin?>(value: nil) var avatarPickerHolder: Any?
var pendingAvatar: CreatePendingPeerAvatar?
let uploadedAvatar = Promise<UploadedPeerPhotoData>() let applyPendingAvatar: (CreatePendingPeerAvatar) -> Void = { avatar in
var uploadedVideoAvatar: (Promise<UploadedPeerPhotoData?>, Double?)? = nil 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 { if initialTitle == nil && peerIds.count > 0 && peerIds.count < 5 {
let _ = (context.engine.data.get( let _ = (context.engine.data.get(
@ -793,18 +816,14 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
let _ = createSignal let _ = createSignal
let _ = replaceControllerImpl let _ = replaceControllerImpl
let _ = dismissImpl let _ = dismissImpl
let _ = uploadedVideoAvatar
actionsDisposable.add((createSignal actionsDisposable.add((createSignal
|> mapToSignal { result -> Signal<CreateGroupResult?, CreateGroupError> in |> mapToSignal { result -> Signal<CreateGroupResult?, CreateGroupError> in
guard let result = result else { guard let result = result else {
return .single(nil) return .single(nil)
} }
let updatingAvatar = stateValue.with { if let pendingAvatar {
return $0.avatar return context.engine.peers.updatePeerPhoto(peerId: result.peerId, photo: pendingAvatar.uploadedPhoto, video: pendingAvatar.uploadedVideo, videoStartTimestamp: pendingAvatar.videoStartTimestamp, markup: pendingAvatar.markup, mapResourceToAvatarSizes: { resource, representations in
}
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
return mapResourceToAvatarSizes(engine: context.engine, resource: resource, representations: representations) return mapResourceToAvatarSizes(engine: context.engine, resource: resource, representations: representations)
}) })
|> ignoreValues |> ignoreValues
@ -893,216 +912,134 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
}, changeProfilePhoto: { }, changeProfilePhoto: {
endEditingImpl?() endEditingImpl?()
let title = stateValue.with { state -> String in let peerType: AvatarEditorScreen.PeerType
return state.editingName.composedTitle if case let .requestPeer(group) = mode, group.isForum == true {
peerType = .forum
} else {
peerType = .group
} }
let _ = (context.engine.data.get( let keyboardInputData = Promise<AvatarKeyboardInputData>()
TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId), keyboardInputData.set(AvatarEditorScreen.inputData(context: context, isGroup: true))
TelegramEngine.EngineData.Item.Configuration.SearchBots()
) var dismissPickerImpl: (() -> Void)?
|> deliverOnMainQueue).start(next: { peer, searchBotsConfiguration in let (mainController, pickerHolder) = context.sharedContext.makeAvatarMediaPickerScreen(context: context, getSourceRect: { return nil }, canDelete: pendingAvatar != nil, performDelete: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 } clearPendingAvatar()
}, completion: { result, transitionView, transitionRect, transitionImage, fromCamera, _, cancelled in
avatarPickerHolder = nil
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme) let applyPhoto: (UIImage) -> Void = { image in
legacyController.statusBar.statusBarStyle = .Ignore if let avatar = CreatePeerAvatarSetup.photo(context: context, image: image) {
applyPendingAvatar(avatar)
let emptyController = LegacyEmptyController(context: legacyController.context)! }
let navigationController = makeLegacyNavigationController(rootController: emptyController) }
navigationController.setNavigationBarHidden(true, animated: false) let applyVideo: (UIImage, MediaEditorScreenImpl.MediaResult.VideoResult?, MediaEditorValues?, UploadPeerPhotoMarkup?) -> Void = { image, video, values, markup in
navigationController.navigationBar.transform = CGAffineTransform(translationX: -1000.0, y: 0.0) if let avatar = CreatePeerAvatarSetup.video(context: context, image: image, video: video, values: values, markup: markup, didCompleteLoadingPreview: { avatar in
updatePendingAvatarIfCurrent(avatar)
legacyController.bind(controller: navigationController) }) {
applyPendingAvatar(avatar)
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 completedGroupVideoImpl: (UIImage, Any?, TGVideoEditAdjustments?) -> Void = { image, asset, adjustments in let subject: Signal<MediaEditorScreenImpl.Subject?, NoError>
if let data = image.jpegData(compressionQuality: 0.6) { if let asset = result as? PHAsset {
let photoResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) subject = .single(.asset(asset))
context.engine.resources.storeResourceData(id: EngineMediaResource.Id(photoResource.id), data: data) } else if let image = result as? UIImage {
let representation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 640, height: 640), resource: photoResource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false) subject = .single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight, fromCamera: false))
updateState { state in } else if let result = result as? Signal<CameraScreenImpl.Result, NoError> {
var state = state subject = result
state.avatar = .image(representation, true) |> map { value -> MediaEditorScreenImpl.Subject? in
return state 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<TelegramMediaResource?, UploadPeerPhotoError> { 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<UploadedPeerPhotoData?>()
promise.set(signal
|> `catch` { _ -> Signal<TelegramMediaResource?, NoError> in
return .single(nil)
}
|> mapToSignal { resource -> Signal<UploadedPeerPhotoData?, NoError> 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<AvatarKeyboardInputData>() let editorController = MediaEditorScreenImpl(
keyboardInputData.set(AvatarEditorScreen.inputData(context: context, isGroup: true)) context: context,
mode: .avatarEditor,
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)! subject: subject,
mixin.stickersContext = LegacyPaintStickersContext(context: context) transitionIn: fromCamera ? .camera : transitionView.flatMap({ .gallery(
let _ = currentAvatarMixin.swap(mixin) MediaEditorScreenImpl.TransitionIn.GalleryTransitionIn(
mixin.requestSearchController = { assetsController in sourceView: $0,
let controller = WebSearchController(context: context, peer: peer, chatLocation: nil, configuration: searchBotsConfiguration, mode: .avatar(initialQuery: title, completion: { result in sourceRect: transitionRect,
assetsController?.dismiss() sourceImage: transitionImage
completedGroupPhotoImpl(result) )
})) ) }),
presentControllerImpl?(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet)) transitionOut: { finished, _ in
} if !finished, let transitionView {
// mixin.requestAvatarEditor = { imageCompletion, videoCompletion in return MediaEditorScreenImpl.TransitionOut(
// guard let imageCompletion, let videoCompletion else { destinationView: transitionView,
// return destinationRect: transitionView.bounds,
// } destinationCornerRadius: 0.0
// 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
} }
uploadedAvatar.set(.never()) return nil
} },
} completion: { results, commit in
mixin.didDismiss = { [weak legacyController] in guard let result = results.first else {
let _ = currentAvatarMixin.swap(nil) return
legacyController?.dismiss() }
} switch result.media {
let menuController = mixin.present() case let .image(image, _):
if let menuController = menuController { applyPhoto(image)
menuController.customRemoveFromParentViewController = { [weak legacyController] in commit({})
legacyController?.dismiss() 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: { }, changeLocation: {
endEditingImpl?() endEditingImpl?()
@ -1313,6 +1250,8 @@ public func createGroupControllerImpl(context: AccountContext, peerIds: [PeerId]
} }
|> afterDisposed { |> afterDisposed {
actionsDisposable.dispose() actionsDisposable.dispose()
let _ = avatarPickerHolder
} }
let controller = ItemListController(context: context, state: signal) let controller = ItemListController(context: context, state: signal)

View file

@ -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<UploadedPeerPhotoData, NoError>
let uploadedVideo: Signal<UploadedPeerPhotoData?, NoError>?
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<TelegramMediaResource?, UploadPeerPhotoError>
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<TelegramMediaResource?, UploadPeerPhotoError> { 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<TelegramMediaResource?, NoError> in
return .single(nil)
}
|> mapToSignal { resource -> Signal<UploadedPeerPhotoData?, NoError> 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
}
}

View file

@ -43,7 +43,8 @@ final class DisabledContextResultsChatInputContextPanelNode: ChatInputContextPan
self.containerNode.backgroundColor = interfaceState.theme.list.plainBackgroundColor self.containerNode.backgroundColor = interfaceState.theme.list.plainBackgroundColor
self.separatorNode.backgroundColor = interfaceState.theme.list.itemPlainSeparatorColor 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 return
} }
let banDescription: String let banDescription: String

View file

@ -16,7 +16,7 @@ public enum WebSearchMode {
} }
public enum WebSearchControllerMode { 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 editor(completion: (UIImage) -> Void)
case avatar(initialQuery: String?, 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 = { } public var dismissed: () -> Void = { }
@ -261,13 +261,13 @@ public final class WebSearchController: ViewController {
selectionState.setItem(currentItem, selected: true) selectionState.setItem(currentItem, selected: true)
} }
if case let .media(_, sendSelected) = mode { if case let .media(_, sendSelected) = mode {
sendSelected(results, selectionState, editingState, false) sendSelected(results, selectionState, editingState, silently, scheduleTime)
} }
} }
}, schedule: { [weak self] messageEffect in }, schedule: { [weak self] messageEffect in
if let strongSelf = self { if let strongSelf = self {
strongSelf.presentSchedulePicker(false, { [weak self] time in strongSelf.presentSchedulePicker(false, { [weak self] time, silentPosting in
self?.controllerInteraction?.sendSelected(nil, false, time, nil) self?.controllerInteraction?.sendSelected(nil, silentPosting, time, nil)
}) })
} }
}, avatarCompleted: { result in }, avatarCompleted: { result in