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.PrivacyHelpContacts" = "Only your contacts can see your birthday. [Change >]()";
"Settings.Birthday.PrivacyHelpNobody" = "Nobody can see your birthday. [Change >]()";
"GroupPermission.NoSendReactions" = "no reactions";
"Channel.BanUser.PermissionSendReactions" = "Send Reactions";
"Chat.AdminAction.ToastReactionsDeletedTitleSingle" = "Reaction Deleted";
"Chat.AdminAction.ToastReactionsDeletedTextSingle" = "Reaction Deleted.";
"Chat.AdminAction.ToastReactionsDeletedTextMultiple" = "Messages Deleted.";
"Chat.AdminAction.ToastMessagesAndReactionsDeletedText" = "Messages and reactions deleted.";

View file

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

View file

@ -997,7 +997,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
controller.reset = { [weak self, weak controller] in
if let strongSelf = self, let strongController = controller {
strongController.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: suggestReset ? strongSelf.presentationData.strings.TwoStepAuth_RecoveryFailed : strongSelf.presentationData.strings.TwoStepAuth_RecoveryUnavailable, actions: [
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Login_ResetAccountProtected_Reset, action: {
if let strongSelf = self, let strongController = controller {
strongController.inProgress = true
@ -1084,7 +1084,7 @@ public final class AuthorizationSequenceController: NavigationController, ASAuth
controller.reset = { [weak self, weak controller] in
if let strongSelf = self, let strongController = controller {
strongController.present(textAlertController(sharedContext: strongSelf.sharedContext, title: nil, text: strongSelf.presentationData.strings.TwoStepAuth_ResetAccountConfirmation, actions: [
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Login_ResetAccountProtected_Reset, action: {
if let strongSelf = self, let strongController = controller {
strongController.inProgress = true

View file

@ -168,7 +168,7 @@ final class AuthorizationSequencePaymentScreenComponent: Component {
title: nil,
text: errorText,
actions: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {}),
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_OK, action: {}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.Login_PhoneNumberHelp, action: { [weak self] in
guard let self else {
return

View file

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

View file

@ -20,6 +20,7 @@ import SafariServices
import LocationUI
import OpenInExternalAppUI
import GalleryUI
import TextFormat
final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDelegate {
private let context: AccountContext
@ -67,6 +68,8 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
var currentDetailsItems: [InstantPageDetailsItem] = []
private var resolvedExternalMediaDimensions: [MediaId: PixelDimensions] = [:]
private var pendingResolvedExternalMediaDimensions = Set<MediaId>()
private var codeHighlight: CachedMessageSyntaxHighlight?
private var codeHighlightState: (specs: [CachedMessageSyntaxHighlight.Spec], disposable: Disposable)?
var currentAccessibilityAreas: [AccessibilityAreaNode] = []
@ -90,6 +93,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
private let resolveUrlDisposable = MetaDisposable()
private let updateLayoutDisposable = MetaDisposable()
private let updateExternalMediaDimensionsDisposable = MetaDisposable()
private let updateCodeHighlightDisposable = MetaDisposable()
private let loadProgress = ValuePromise<CGFloat>(1.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.updateCodeHighlight()
}
deinit {
@ -199,6 +205,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
self.resolveUrlDisposable.dispose()
self.updateLayoutDisposable.dispose()
self.updateExternalMediaDimensionsDisposable.dispose()
self.updateCodeHighlightDisposable.dispose()
}
required init?(coder: NSCoder) {
@ -325,6 +332,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
}
}
self.currentLayout = nil
self.updateCodeHighlight()
self.updatePageLayout()
self.scrollNode.frame = CGRect(x: 0.0, y: 0.0, width: 1.0, height: 1.0)
@ -490,7 +498,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
return
}
let currentLayout = instantPageLayoutForWebPage(webPage, instantPage: instantPage, userLocation: self.sourceLocation.userLocation, boundingWidth: size.width, safeInset: insets.left, strings: self.presentationData.strings, theme: self.theme, dateTimeFormat: self.presentationData.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights)
let currentLayout = instantPageLayoutForWebPage(webPage, instantPage: instantPage, userLocation: self.sourceLocation.userLocation, boundingWidth: size.width, safeInset: insets.left, strings: self.presentationData.strings, theme: self.theme, dateTimeFormat: self.presentationData.dateTimeFormat, webEmbedHeights: self.currentWebEmbedHeights, cachedMessageSyntaxHighlight: self.codeHighlight)
let currentLayoutTiles = instantPageTilesFromLayout(currentLayout, boundingWidth: size.width)
@ -551,6 +559,96 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
self.scrollNode.view.contentSize = currentLayout.contentSize
self.scrollNodeFooter.frame = CGRect(origin: CGPoint(x: 0.0, y: currentLayout.contentSize.height), size: CGSize(width: size.width, height: 2000.0))
}
private func updateCodeHighlight() {
guard let instantPage = self.webPage?.instantPage else {
self.codeHighlight = nil
self.codeHighlightState = nil
self.updateCodeHighlightDisposable.set(nil)
return
}
let specs = syntaxHighlightSpecs(for: instantPage.blocks)
if let currentState = self.codeHighlightState, currentState.specs == specs {
return
}
if specs.isEmpty {
let hadHighlight = self.codeHighlight != nil
self.codeHighlight = nil
self.codeHighlightState = nil
self.updateCodeHighlightDisposable.set(nil)
if hadHighlight {
self.updatePageLayout()
self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds)
}
return
}
let disposable = MetaDisposable()
self.codeHighlightState = (specs, disposable)
self.updateCodeHighlightDisposable.set(disposable)
disposable.set((asyncStanaloneSyntaxHighlight(current: self.codeHighlight, specs: specs)
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
return
}
if self.codeHighlight != result {
self.codeHighlight = result
self.updatePageLayout()
self.updateVisibleItems(visibleBounds: self.scrollNode.view.bounds)
}
}))
}
private func syntaxHighlightSpecs(for blocks: [InstantPageBlock]) -> [CachedMessageSyntaxHighlight.Spec] {
var specs: [CachedMessageSyntaxHighlight.Spec] = []
var seen = Set<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) {
var visibleTileIndices = Set<Int>()

View file

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

View file

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

View file

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

View file

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

View file

@ -57,6 +57,18 @@ public extension UnicodeScalar {
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 {
func trimmingTrailingSpaces() -> String {
var t = self
@ -74,6 +86,20 @@ public extension String {
return self.contains { $0.isEmoji }
}
var containsGraphicEmoji: Bool {
var containsEmoji = false
self.enumerateSubstrings(in: self.startIndex ..< self.endIndex, options: .byComposedCharacterSequences) { substring, _, _, stop in
guard let substring else {
return
}
if substring.containsEmoji && !allowedEmojiLikeSymbols.contains(substring) {
containsEmoji = true
stop = true
}
}
return containsEmoji
}
var containsOnlyEmoji: Bool {
return !self.isEmpty && !self.contains { !$0.isEmoji }
}

View file

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

View file

@ -6,6 +6,7 @@ import TelegramPresentationData
import TelegramUIPreferences
import TelegramStringFormatting
import MosaicLayout
import TextFormat
public final class InstantPageLayout {
public let origin: CGPoint
@ -28,8 +29,7 @@ public final class InstantPageLayout {
}
}
private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantPageTheme, category: InstantPageTextCategoryType, link: Bool) {
let attributes = theme.textCategories.attributes(type: category, link: link)
private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantPageTheme, attributes: InstantPageTextAttributes) {
stack.push(.textColor(attributes.color))
stack.push(.markerColor(theme.markerColor))
stack.push(.linkColor(theme.linkColor))
@ -47,7 +47,91 @@ private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantP
}
}
public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: MediaResourceUserLocation, rtl: Bool, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, safeInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToSize: CGSize?, media: [EngineMedia.Id: EngineMedia], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, detailsIndexCounter: inout Int, theme: InstantPageTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], excludeCaptions: Bool) -> InstantPageLayout {
private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantPageTheme, category: InstantPageTextCategoryType, link: Bool) {
setupStyleStack(stack, theme: theme, attributes: theme.textCategories.attributes(type: category, link: link))
}
private func instantPageFont(style: InstantPageTextAttributes, bold: Bool = false, italic: Bool = false, fixed: Bool = false) -> UIFont {
let size = style.font.size
if fixed {
if bold && italic {
return UIFont(name: "Menlo-BoldItalic", size: size) ?? Font.semiboldItalic(size)
} else if bold {
return UIFont(name: "Menlo-Bold", size: size) ?? Font.bold(size)
} else if italic {
return UIFont(name: "Menlo-Italic", size: size) ?? Font.italic(size)
} else {
return UIFont(name: "Menlo", size: size) ?? Font.regular(size)
}
}
switch style.font.style {
case .serif:
if bold && italic {
return UIFont(name: "Georgia-BoldItalic", size: size) ?? Font.semiboldItalic(size)
} else if bold {
return UIFont(name: "Georgia-Bold", size: size) ?? Font.bold(size)
} else if italic {
return UIFont(name: "Georgia-Italic", size: size) ?? Font.italic(size)
} else {
return UIFont(name: "Georgia", size: size) ?? Font.regular(size)
}
case .sans:
if bold && italic {
return Font.semiboldItalic(size)
} else if bold {
return Font.bold(size)
} else if italic {
return Font.italic(size)
} else {
return Font.regular(size)
}
}
}
private func attributedStringForPreformattedText(_ text: RichText, language: String?, theme: InstantPageTheme, cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight?) -> NSAttributedString {
let paragraphAttributes = theme.textCategories.attributes(type: .paragraph, link: false)
let textValue = text.plainText
guard !textValue.isEmpty else {
return NSAttributedString(
string: "",
attributes: [
.font: instantPageFont(style: paragraphAttributes, fixed: true),
.foregroundColor: paragraphAttributes.color,
NSAttributedString.Key(rawValue: InstantPageLineSpacingFactorAttribute): paragraphAttributes.font.lineSpacingFactor as NSNumber
]
)
}
let attributedString = stringWithAppliedEntities(
textValue,
entities: [
MessageTextEntity(range: 0 ..< (textValue as NSString).length, type: .Pre(language: language))
],
baseColor: paragraphAttributes.color,
linkColor: theme.linkColor,
codeBlockTitleColor: paragraphAttributes.color,
codeBlockAccentColor: paragraphAttributes.color,
codeBlockBackgroundColor: theme.codeBlockBackgroundColor,
baseFont: instantPageFont(style: paragraphAttributes),
linkFont: instantPageFont(style: paragraphAttributes),
boldFont: instantPageFont(style: paragraphAttributes, bold: true),
italicFont: instantPageFont(style: paragraphAttributes, italic: true),
boldItalicFont: instantPageFont(style: paragraphAttributes, bold: true, italic: true),
fixedFont: instantPageFont(style: paragraphAttributes, fixed: true),
blockQuoteFont: instantPageFont(style: paragraphAttributes),
underlineLinks: false,
message: nil,
cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight
).mutableCopy() as! NSMutableAttributedString
attributedString.addAttribute(
NSAttributedString.Key(rawValue: InstantPageLineSpacingFactorAttribute),
value: paragraphAttributes.font.lineSpacingFactor as NSNumber,
range: NSRange(location: 0, length: attributedString.length)
)
return attributedString
}
public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: MediaResourceUserLocation, rtl: Bool, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, safeInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToSize: CGSize?, media: [EngineMedia.Id: EngineMedia], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, detailsIndexCounter: inout Int, theme: InstantPageTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil, excludeCaptions: Bool) -> InstantPageLayout {
let layoutCaption: (InstantPageCaption, CGSize) -> ([InstantPageItem], CGSize) = { caption, contentSize in
var items: [InstantPageItem] = []
@ -100,7 +184,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
switch block {
case let .cover(block):
return layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: true, previousItems:previousItems, fillToSize: fillToSize, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false)
return layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: true, previousItems:previousItems, fillToSize: fillToSize, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false)
case let .title(text):
let styleStack = InstantPageTextStyleStack()
setupStyleStack(styleStack, theme: theme, category: .header, link: false)
@ -168,16 +252,34 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
setupStyleStack(styleStack, theme: theme, category: .subheader, link: false)
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage)
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
case let .heading(text, level):
let styleStack = InstantPageTextStyleStack()
setupStyleStack(styleStack, theme: theme, attributes: theme.headingTextAttributes(level: level, link: false))
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage)
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
case let .paragraph(text):
let styleStack = InstantPageTextStyleStack()
setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false)
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0, horizontalInset: horizontalInset, offset: CGPoint(x: horizontalInset, y: 0.0), media: media, webpage: webpage)
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
case let .preformatted(text):
let styleStack = InstantPageTextStyleStack()
setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false)
case let .preformatted(text, language):
let backgroundInset: CGFloat = 14.0
let (_, items, contentSize) = layoutTextItemWithString(attributedStringForRichText(text, styleStack: styleStack), boundingWidth: boundingWidth - horizontalInset * 2.0 - backgroundInset * 2.0, offset: CGPoint(x: 17.0, y: backgroundInset), media: media, webpage: webpage, opaqueBackground: true)
let attributedString: NSAttributedString
if let language, !language.isEmpty {
attributedString = attributedStringForPreformattedText(text, language: language, theme: theme, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight)
} else {
let styleStack = InstantPageTextStyleStack()
setupStyleStack(styleStack, theme: theme, category: .paragraph, link: false)
attributedString = attributedStringForRichText(text, styleStack: styleStack)
}
let (_, items, contentSize) = layoutTextItemWithString(
attributedString,
boundingWidth: boundingWidth - horizontalInset * 2.0 - backgroundInset * 2.0,
offset: CGPoint(x: 17.0, y: backgroundInset),
media: media,
webpage: webpage,
opaqueBackground: true
)
let backgroundItem = InstantPageShapeItem(frame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: contentSize.height + backgroundInset * 2.0)), shapeFrame: CGRect(origin: CGPoint(), size: CGSize(width: boundingWidth, height: contentSize.height + backgroundInset * 2.0)), shape: .rect, color: theme.codeBlockBackgroundColor)
var allItems: [InstantPageItem] = [backgroundItem]
allItems.append(contentsOf: items)
@ -274,7 +376,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var previousBlock: InstantPageBlock?
var originY: CGFloat = contentSize.height
for subBlock in blocks {
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - indexSpacing - maxIndexWidth, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: listItems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false)
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - indexSpacing - maxIndexWidth, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: listItems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false)
let spacing: CGFloat = previousBlock != nil && subLayout.contentSize.height > 0.0 ? spacingBetweenBlocks(upper: previousBlock, lower: subBlock) : 0.0
let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: horizontalInset + indexSpacing + maxIndexWidth, y: contentSize.height + spacing))
@ -476,7 +578,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var i = 0
for subItem in innerItems {
let frame = mosaicLayout[i].0
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subItem, boundingWidth: frame.width, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: frame.size, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: true)
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subItem, boundingWidth: frame.width, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: frame.size, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: true)
items.append(contentsOf: subLayout.flattenedItemsWithOrigin(frame.origin))
i += 1
}
@ -550,7 +652,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var previousBlock: InstantPageBlock?
for subBlock in blocks {
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false)
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth - horizontalInset * 2.0 - lineInset, horizontalInset: 0.0, safeInset: 0.0, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false)
let spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock)
let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: horizontalInset + lineInset, y: contentSize.height + spacing))
@ -733,7 +835,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var previousBlock: InstantPageBlock?
for subBlock in blocks {
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: false, previousItems: subitems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &subDetailsIndex, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false)
let subLayout = layoutInstantPageBlock(webpage: webpage, userLocation: userLocation, rtl: rtl, block: subBlock, boundingWidth: boundingWidth, horizontalInset: horizontalInset, safeInset: safeInset, isCover: false, previousItems: subitems, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &subDetailsIndex, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false)
let spacing = spacingBetweenBlocks(upper: previousBlock, lower: subBlock)
let blockItems = subLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing))
@ -842,7 +944,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
}
}
public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, instantPage: InstantPage?, userLocation: MediaResourceUserLocation, boundingWidth: CGFloat, safeInset: CGFloat, strings: PresentationStrings, theme: InstantPageTheme, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:]) -> InstantPageLayout {
public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, instantPage: InstantPage?, userLocation: MediaResourceUserLocation, boundingWidth: CGFloat, safeInset: CGFloat, strings: PresentationStrings, theme: InstantPageTheme, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], cachedMessageSyntaxHighlight: CachedMessageSyntaxHighlight? = nil) -> InstantPageLayout {
var maybeLoadedContent: TelegramMediaWebpageLoadedContent?
if case let .Loaded(content) = webPage.content {
maybeLoadedContent = content
@ -871,7 +973,7 @@ public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, instant
var previousBlock: InstantPageBlock?
for block in pageBlocks {
let blockLayout = layoutInstantPageBlock(webpage: webPage, userLocation: userLocation, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: 17.0 + safeInset, safeInset: safeInset, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, excludeCaptions: false)
let blockLayout = layoutInstantPageBlock(webpage: webPage, userLocation: userLocation, rtl: rtl, block: block, boundingWidth: boundingWidth, horizontalInset: 17.0 + safeInset, safeInset: safeInset, isCover: false, previousItems: items, fillToSize: nil, media: media, mediaIndexCounter: &mediaIndexCounter, embedIndexCounter: &embedIndexCounter, detailsIndexCounter: &detailsIndexCounter, theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, webEmbedHeights: webEmbedHeights, cachedMessageSyntaxHighlight: cachedMessageSyntaxHighlight, excludeCaptions: false)
let spacing = spacingBetweenBlocks(upper: previousBlock, lower: block)
let blockItems = blockLayout.flattenedItemsWithOrigin(CGPoint(x: 0.0, y: contentSize.height + spacing))
items.append(contentsOf: blockItems)

View file

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

View file

@ -135,6 +135,32 @@ public final class InstantPageTheme {
public func withUpdatedFontStyles(sizeMultiplier: CGFloat, forceSerif: Bool) -> InstantPageTheme {
return InstantPageTheme(type: type, pageBackgroundColor: pageBackgroundColor, textCategories: self.textCategories.withUpdatedFontStyles(sizeMultiplier: sizeMultiplier, forceSerif: forceSerif), serif: forceSerif, codeBlockBackgroundColor: codeBlockBackgroundColor, linkColor: linkColor, textHighlightColor: textHighlightColor, linkHighlightColor: linkHighlightColor, markerColor: markerColor, panelBackgroundColor: panelBackgroundColor, panelHighlightedBackgroundColor: panelHighlightedBackgroundColor, panelPrimaryColor: panelPrimaryColor, panelSecondaryColor: panelSecondaryColor, panelAccentColor: panelAccentColor, tableBorderColor: tableBorderColor, tableHeaderColor: tableHeaderColor, controlColor: controlColor, imageTintColor: imageTintColor, overlayPanelColor: overlayPanelColor)
}
func headingTextAttributes(level: Int32, link: Bool) -> InstantPageTextAttributes {
let clampedLevel = max(Int32(3), min(level, Int32(6)))
let subheaderAttributes = self.textCategories.subheader
guard clampedLevel > 3 else {
return subheaderAttributes.withUnderline(link)
}
let baseSize: CGFloat
switch clampedLevel {
case 4:
baseSize = 17.0
case 5:
baseSize = 15.0
default:
baseSize = 13.0
}
let sizeMultiplier = subheaderAttributes.font.size / 19.0
let attributes = InstantPageTextAttributes(
font: InstantPageFont(style: .serif, size: floor(baseSize * sizeMultiplier), lineSpacingFactor: subheaderAttributes.font.lineSpacingFactor),
color: subheaderAttributes.color,
underline: subheaderAttributes.underline
)
return attributes.withUnderline(link)
}
}
private let lightTheme = InstantPageTheme(

View file

@ -35,7 +35,7 @@
@property (nonatomic) bool reminder;
@property (nonatomic) bool forum;
@property (nonatomic) bool isSuggesting;
@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t));
@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t, bool));
@property (nonatomic, copy) void (^presentTimerController)(void (^)(int32_t));
@property (nonatomic, strong) NSArray *underlyingViews;

View file

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

View file

@ -35,7 +35,7 @@
@property (nonatomic, assign) bool forum;
@property (nonatomic, assign) bool isSuggesting;
@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t));
@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t, bool));
@property (nonatomic, copy) void (^presentTimerController)(void (^)(int32_t));
@property (nonatomic, assign) CGFloat topInset;

View file

@ -28,7 +28,7 @@
@property (nonatomic, copy) void (^editorOpened)(void);
@property (nonatomic, copy) void (^editorClosed)(void);
@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t));
@property (nonatomic, copy) void (^presentScheduleController)(bool, void (^)(int32_t, bool));
@property (nonatomic, copy) void (^presentTimerController)(void (^)(int32_t));
- (instancetype)initWithContext:(id<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;
typedef void (^ _Nonnull TGPhotoVideoEditorSchedulePickerCompletion)(int32_t time);
typedef void (^ _Nonnull TGPhotoVideoEditorSchedulePickerCompletion)(int32_t time, bool silentPosting);
typedef void (^ _Nonnull TGPhotoVideoEditorSchedulePicker)(bool media, TGPhotoVideoEditorSchedulePickerCompletion _Nonnull done);
typedef void (^ _Nonnull TGPhotoVideoEditorCompletion)(id<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(^didStop)(void);
@property (nonatomic, copy) void(^displaySlowmodeTooltip)(void);
@property (nonatomic, copy) void (^presentScheduleController)(void (^)(int32_t));
@property (nonatomic, copy) void (^presentScheduleController)(void (^)(int32_t, bool));
- (instancetype)initWithContext:(id<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)
return;
strongSelf.presentScheduleController(true, ^(int32_t time) {
strongSelf.presentScheduleController(true, ^(int32_t time, bool silentPosting) {
__strong TGCameraController *strongSelf = weakSelf;
__strong TGMediaPickerGalleryModel *strongModel = weakModel;
@ -1678,7 +1678,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
[[NSUserDefaults standardUserDefaults] setObject:@(!strongSelf->_selectionContext.grouping) forKey:@"TG_mediaGroupingDisabled_v0"];
if (strongSelf.finishedWithResults != nil)
strongSelf.finishedWithResults(strongController, strongSelf->_selectionContext, strongSelf->_editingContext, item.asset, false, time);
strongSelf.finishedWithResults(strongController, strongSelf->_selectionContext, strongSelf->_editingContext, item.asset, silentPosting, time);
[strongSelf _dismissTransitionForResultController:strongController];
});

View file

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

View file

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

View file

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

View file

@ -844,13 +844,13 @@ typedef enum
}
if (strongSelf.presentScheduleController) {
strongSelf.presentScheduleController(^(int32_t time) {
strongSelf.presentScheduleController(^(int32_t time, bool silentPosting) {
__strong TGVideoMessageCaptureController *strongSelf = weakSelf;
if (strongSelf == nil) {
return;
}
[strongSelf finishWithURL:strongSelf->_url dimensions:CGSizeMake(240.0f, 240.0f) duration:strongSelf->_duration liveUploadData:strongSelf->_liveUploadData thumbnailImage:strongSelf->_thumbnailImage isSilent:false scheduleTimestamp:time];
[strongSelf finishWithURL:strongSelf->_url dimensions:CGSizeMake(240.0f, 240.0f) duration:strongSelf->_duration liveUploadData:strongSelf->_liveUploadData thumbnailImage:strongSelf->_thumbnailImage isSilent:silentPosting scheduleTimestamp:time];
_automaticDismiss = true;
[strongSelf dismiss:false];

View file

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

View file

@ -20,7 +20,7 @@ public func guessMimeTypeByFileExtension(_ ext: String) -> String {
return TGMimeTypeMap.mimeType(forExtension: ext) ?? "application/binary"
}
public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, context: AccountContext, peer: Peer, chatLocation: ChatLocation, captionsEnabled: Bool = true, storeCreatedAssets: Bool = true, showFileTooltip: Bool = false, initialCaption: NSAttributedString, hasSchedule: Bool, presentWebSearch: (() -> Void)?, presentSelectionLimitExceeded: @escaping () -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?) {
public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, context: AccountContext, peer: Peer, chatLocation: ChatLocation, captionsEnabled: Bool = true, storeCreatedAssets: Bool = true, showFileTooltip: Bool = false, initialCaption: NSAttributedString, hasSchedule: Bool, presentWebSearch: (() -> Void)?, presentSelectionLimitExceeded: @escaping () -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32, Bool) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?) {
let paintStickersContext = LegacyPaintStickersContext(context: context)
paintStickersContext.captionPanelView = {
return getCaptionPanelView()
@ -43,8 +43,8 @@ public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, co
controller.hasCoverButton = true
}
controller.presentScheduleController = { media, done in
presentSchedulePicker(media, { time in
done?(time)
presentSchedulePicker(media, { time, silentPosting in
done?(time, silentPosting)
})
}
controller.presentTimerController = { done in

View file

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

View file

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

View file

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

View file

@ -697,7 +697,7 @@ final class SetupTwoStepVerificationControllerNode: ViewControllerTracingNode {
}, transition: .animated(duration: 0.5, curve: .spring))
}
if case let .enterEmail(enterEmailState, enterEmailEmail)? = self.innerState.data.state, case .create = enterEmailState, enterEmailEmail.isEmpty {
self.present(textAlertController(sharedContext: self.context.sharedContext, title: nil, text: self.presentationData.strings.TwoStepAuth_EmailSkipAlert, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: self.presentationData.strings.TwoStepAuth_EmailSkip, action: {
self.present(textAlertController(sharedContext: self.context.sharedContext, title: nil, text: self.presentationData.strings.TwoStepAuth_EmailSkipAlert, actions: [TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: self.presentationData.strings.TwoStepAuth_EmailSkip, action: {
continueImpl()
})]), nil)
} else {

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

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

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

View file

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

View file

@ -313,7 +313,7 @@ func createPasswordController(context: AccountContext, createPasswordContext: Cr
}
if emailAlert {
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.TwoStepAuth_EmailSkipAlert, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: presentationData.strings.TwoStepAuth_EmailSkip, action: {
presentControllerImpl?(textAlertController(context: context, title: nil, text: presentationData.strings.TwoStepAuth_EmailSkipAlert, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}), TextAlertAction(type: .destructiveAction, title: presentationData.strings.TwoStepAuth_EmailSkip, action: {
saveImpl()
})]), nil)
} else {

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: {
self?.decline()
}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
})], actionLayout: .vertical), in: .window(.root))
}, rightAction: { [weak self] in
guard let strongSelf = self else {

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

View file

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

View file

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

View file

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

View file

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

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) {
if peerId.namespace == Namespaces.Peer.SecretChat {
var resourceIds: [MediaResourceId] = []

View file

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

View file

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

View file

@ -2634,7 +2634,7 @@ public class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDeleg
size: CGSize(width: baseWidth, height: panelHeight)
)
let audioRecordingTimeFrame = CGRect(origin: CGPoint(x: hideOffset.x + leftInset + leftMenuInset + 8.0 + 34.0, y: (accessoryPanel != nil ? 52.0 : 0.0) + panelHeight - minimalHeight + floor((minimalHeight - audioRecordingTimeSize.height) / 2.0) + 1.0 - UIScreenPixel), size: audioRecordingTimeSize)
let audioRecordingTimeFrame = CGRect(origin: CGPoint(x: hideOffset.x + leftInset + leftMenuInset + 8.0 + 22.0, y: (accessoryPanel != nil ? 52.0 : 0.0) + panelHeight - minimalHeight + floor((minimalHeight - audioRecordingTimeSize.height) / 2.0) + 1.0 - UIScreenPixel), size: audioRecordingTimeSize)
if animateTimeSlideIn {
var previousAudioRecordingTimeFrame = audioRecordingTimeFrame

View file

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

View file

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

View file

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

View file

@ -620,7 +620,7 @@ private final class GiftViewSheetContent: CombinedComponent {
title: presentationData.strings.Gift_Convert_Title,
text: text,
actions: [
TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .genericAction, title: presentationData.strings.Common_Cancel, action: {}),
TextAlertAction(type: .defaultAction, title: presentationData.strings.Gift_Convert_Convert, action: { [weak self, weak controller, weak navigationController] in
guard let self else {
return

View file

@ -11,7 +11,7 @@ import ShareController
import LegacyUI
import LegacyMediaPickerUI
public func presentedLegacyCamera(context: AccountContext, peer: Peer?, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, attachmentController: ViewController? = nil, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: NSAttributedString, hasSchedule: Bool, enablePhoto: Bool, enableVideo: Bool, sendPaidMessageStars: Int64 = 0, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ChatSendMessageActionSheetController.SendParameters?) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, dismissedWithResult: @escaping () -> Void = {}, finishedTransitionIn: @escaping () -> Void = {}) {
public func presentedLegacyCamera(context: AccountContext, peer: Peer?, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, attachmentController: ViewController? = nil, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: NSAttributedString, hasSchedule: Bool, enablePhoto: Bool, enableVideo: Bool, sendPaidMessageStars: Int64 = 0, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32, ChatSendMessageActionSheetController.SendParameters?) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32, Bool) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, dismissedWithResult: @escaping () -> Void = {}, finishedTransitionIn: @escaping () -> Void = {}) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme)
legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait)
@ -45,8 +45,8 @@ public func presentedLegacyCamera(context: AccountContext, peer: Peer?, chatLoca
}
controller.presentScheduleController = { _, done in
presentSchedulePicker(true, { time in
done?(time)
presentSchedulePicker(true, { time, silentPosting in
done?(time, silentPosting)
})
}
controller.presentTimerController = { done in

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

View file

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

View file

@ -4533,7 +4533,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
TextAlertAction(type: .destructiveAction, title: actionText, action: {
self?.deletePeerChat(peer: peer._asPeer(), globally: delete)
}),
TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
})
], parseMarkdown: true), in: .window(.root))
})

View file

@ -5177,7 +5177,7 @@ public final class PeerInfoStoryPaneNode: ASDisplayNode, PeerInfoPaneNode, ASScr
public func presentDeleteBotPreviewLanguage() {
self.parentController?.present(textAlertController(context: self.context, title: self.presentationData.strings.BotPreviews_DeleteTranslationAlert_Title, text: self.presentationData.strings.BotPreviews_DeleteTranslationAlert_Text, actions: [
TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {
TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {
}),
TextAlertAction(type: .destructiveAction, title: self.presentationData.strings.Common_OK, action: { [weak self] in
guard let self else {

View file

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

View file

@ -34,6 +34,8 @@ import TelegramAccountAuxiliaryMethods
import PeerSelectionController
import ContextMenuScreen
import NavigationBarImpl
import ContextUI
import ContextControllerImpl
private var installedSharedLogger = false
@ -201,6 +203,18 @@ public class ShareRootControllerImpl {
defaultNavigationBarImpl = { presentationData in
return NavigationBarImpl(presentationData: presentationData)
}
makeContextControllerImpl = { context, presentationData, configuration, recognizer, gesture, workaroundUseLegacyImplementation, disableScreenshots, hideReactionPanelTail in
return ContextControllerImpl(
context: context,
presentationData: presentationData,
configuration: configuration,
recognizer: recognizer,
gesture: gesture,
workaroundUseLegacyImplementation: workaroundUseLegacyImplementation,
disableScreenshots: disableScreenshots,
hideReactionPanelTail: hideReactionPanelTail
)
}
}
deinit {

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 {
return
}
@ -987,6 +987,8 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
replyTo: nil,
storyId: focusedStoryId,
content: .contextResult(results, result),
silentPosting: silentPosting,
scheduleTime: scheduleTime,
sendPaidMessageStars: component.slice.additionalPeerData.sendPaidMessageStars
) |> deliverOnMainQueue).start(next: { [weak self, weak view] messageIds in
Queue.mainQueue().after(0.3) {
@ -1140,8 +1142,8 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
guard let self, let view else {
return
}
self.presentScheduleTimePicker(view: view, peer: peer, completion: { time, repeatPeriod in
done(time)
self.presentScheduleTimePicker(view: view, peer: peer, completion: { time, repeatPeriod, silentPosting in
done(time, silentPosting)
})
})))
}
@ -2292,8 +2294,8 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
guard let self, let view else {
return
}
self.presentScheduleTimePicker(view: view, peer: peer, style: media ? .media : .default, completion: { time, repeatPeriod in
done(time)
self.presentScheduleTimePicker(view: view, peer: peer, style: media ? .media : .default, completion: { time, repeatPeriod, silentPosting in
done(time, silentPosting)
})
}
controller.presentTimerPicker = { [weak self, weak view] done in
@ -2372,7 +2374,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
if let view, let component = view.component {
let theme = component.theme
let updatedPresentationData: (initial: PresentationData, signal: Signal<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 {
legacyController.dismiss()
}
@ -2381,14 +2383,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
}
legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak view] result in
if let strongSelf = self, let view {
strongSelf.enqueueChatContextResult(view: view, peer: peer, replyMessageId: replyMessageId, storyId: replyToStoryId, results: results, result: result, hideVia: true)
strongSelf.enqueueChatContextResult(view: view, peer: peer, replyMessageId: replyMessageId, storyId: replyToStoryId, results: results, result: result, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime)
}
}, enqueueMediaMessages: { [weak view] signals in
if let strongSelf = self, let view {
if editingMedia {
strongSelf.editMessageMediaWithLegacySignals(view: view, signals: signals)
} else {
strongSelf.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: replyMessageId, replyToStoryId: replyToStoryId, signals: signals, silentPosting: silentPosting)
strongSelf.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: replyMessageId, replyToStoryId: replyToStoryId, signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime)
}
}
})
@ -2414,8 +2416,8 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
component.controller()?.present(textAlertController(context: component.context, updatedPresentationData: (presentationData, .single(presentationData)), title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}, presentSchedulePicker: { [weak view] media, done in
if let strongSelf = self, let view {
strongSelf.presentScheduleTimePicker(view: view, peer: peer, style: media ? .media : .default, completion: { time, repeatPeriod in
done(time)
strongSelf.presentScheduleTimePicker(view: view, peer: peer, style: media ? .media : .default, completion: { time, repeatPeriod, silentPosting in
done(time, silentPosting)
})
}
}, presentTimerPicker: { [weak view] done in
@ -2640,7 +2642,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
// component.controller()?.push(controller)
}
private func enqueueChatContextResult(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyMessageId: EngineMessage.Id?, storyId: StoryId?, results: ChatContextResultCollection, result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, resetTextInputState: Bool = true) {
private func enqueueChatContextResult(view: StoryItemSetContainerComponent.View, peer: EnginePeer, replyMessageId: EngineMessage.Id?, storyId: StoryId?, results: ChatContextResultCollection, result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, scheduleTime: Int32? = nil, resetTextInputState: Bool = true) {
if !canSendMessagesToPeer(peer._asPeer()) {
return
}
@ -2669,7 +2671,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
}
}
sendMessage(nil)
sendMessage(scheduleTime)
}
private func presentWebSearch(view: StoryItemSetContainerComponent.View, activateOnDisplay: Bool = true, present: @escaping (ViewController, Any?) -> Void) {
@ -2686,14 +2688,14 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots())
|> deliverOnMainQueue).start(next: { [weak self, weak view] configuration in
if let self {
let controller = WebSearchController(context: context, updatedPresentationData: updatedPresentationData, peer: peer, chatLocation: .peer(id: peer.id), configuration: configuration, mode: .media(attachment: true, completion: { [weak self] results, selectionState, editingState, silentPosting in
let controller = WebSearchController(context: context, updatedPresentationData: updatedPresentationData, peer: peer, chatLocation: .peer(id: peer.id), configuration: configuration, mode: .media(attachment: true, completion: { [weak self] results, selectionState, editingState, silentPosting, scheduleTime in
legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak self, weak view] result in
if let self, let view {
self.performSendContextResultAction(view: view, results: results, result: result)
self.performSendContextResultAction(view: view, results: results, result: result, silentPosting: silentPosting, scheduleTime: scheduleTime)
}
}, enqueueMediaMessages: { [weak self, weak view] signals in
if let self, let view, !signals.isEmpty {
self.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: nil, replyToStoryId: StoryId(peerId: peer.id, id: storyId), signals: signals, silentPosting: false)
self.enqueueMediaMessages(view: view, peer: peer, replyToMessageId: nil, replyToStoryId: StoryId(peerId: peer.id, id: storyId), signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime)
}
})
}), activateOnDisplay: activateOnDisplay)
@ -2833,8 +2835,8 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
guard let self, let view else {
return
}
self.presentScheduleTimePicker(view: view, peer: peer, style: .media, completion: { time, repeatPeriod in
done(time)
self.presentScheduleTimePicker(view: view, peer: peer, style: .media, completion: { time, repeatPeriod, silentPosting in
done(time, silentPosting)
})
}, presentTimerPicker: { [weak self, weak view] done in
guard let self, let view else {
@ -2868,7 +2870,7 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
style: ChatScheduleTimeControllerStyle = .default,
selectedTime: Int32? = nil,
dismissByTapOutside: Bool = true,
completion: @escaping (Int32, Int32?) -> Void
completion: @escaping (Int32, Int32?, Bool) -> Void
) {
guard let component = view.component else {
return
@ -2889,16 +2891,25 @@ final class StoryItemSetContainerSendMessage: @unchecked(Sendable) {
sendWhenOnlineAvailable = false
}
let mode: ChatScheduleTimeControllerMode
let mode: ChatScheduleTimeScreen.Mode //ChatScheduleTimeControllerMode
if peer.id == component.context.account.peerId {
mode = .reminders
} else {
mode = .scheduledMessages(sendWhenOnlineAvailable: sendWhenOnlineAvailable)
mode = .scheduledMessages(peerId: peer.id, sendWhenOnlineAvailable: sendWhenOnlineAvailable)
}
let theme = component.theme
let controller = ChatScheduleTimeController(context: component.context, updatedPresentationData: (component.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: theme), component.context.sharedContext.presentationData |> map { $0.withUpdated(theme: theme) }), mode: mode, style: style, currentTime: selectedTime, minimalTime: nil, dismissByTapOutside: dismissByTapOutside, completion: { time in
completion(time, nil)
})
let controller = ChatScheduleTimeScreen(
context: component.context,
mode: mode,
currentTime: selectedTime,
currentRepeatPeriod: nil,
suggestedTime: nil,
minimalTime: nil,
silentPosting: false,
isDark: style == .media,
completion: { result in
completion(result.time, nil, result.silentPosting)
}
)
view.endEditing(true)
view.component?.controller()?.present(controller, in: .window(.root))
})

View file

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

View file

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

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 forwardSourcePeerIds = Set<PeerId>()
@ -1962,7 +1963,7 @@ extension ChatControllerImpl {
}
self.beginDeleteMessagesWithUndo(messageIds: Set(messages.map({ $0.id })), type: .forEveryone)
}),
TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {})
TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {})
],
actionLayout: .vertical
)

View file

@ -182,7 +182,7 @@ extension ChatControllerImpl {
viewOnceAvailable: viewOnceAvailable,
inputPanelFrame: (currentInputPanelFrame, self.chatDisplayNode.inputNode != nil),
chatNode: self.chatDisplayNode.historyNode,
completion: { [weak self] message, silentPosting, scheduleTime in
completion: { [weak self] message, silentPosting, scheduleTime, repeatPeriod in
guard let self, let videoController = self.videoRecorderValue else {
return
}
@ -230,14 +230,8 @@ extension ChatControllerImpl {
}, usedCorrelationId ? correlationId : nil)
let messages = [message]
let transformedMessages: [EnqueueMessage]
if let silentPosting {
transformedMessages = self.transformEnqueueMessages(messages, silentPosting: silentPosting)
} else if let scheduleTime {
transformedMessages = self.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime)
} else {
transformedMessages = self.transformEnqueueMessages(messages)
}
let effectiveSilentPosting = silentPosting ?? self.presentationInterfaceState.interfaceState.silentPosting
let transformedMessages = self.transformEnqueueMessages(messages, silentPosting: effectiveSilentPosting, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod)
self.sendMessages(transformedMessages)
}
@ -758,14 +752,8 @@ extension ChatControllerImpl {
let messages: [EnqueueMessage] = [.message(text: "", attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.LocalFile, id: Int64.random(in: Int64.min ... Int64.max)), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: Int64(audio.fileSize), attributes: [.Audio(isVoice: true, duration: finalDuration, title: nil, performer: nil, waveform: waveformBuffer)], alternativeRepresentations: [])), threadId: self.chatLocation.threadId, replyToMessageId: self.presentationInterfaceState.interfaceState.replyMessageSubject?.subjectModel, replyToStoryId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])]
let transformedMessages: [EnqueueMessage]
if let silentPosting = silentPosting {
transformedMessages = self.transformEnqueueMessages(messages, silentPosting: silentPosting, postpone: postpone)
} else if let scheduleTime = scheduleTime {
transformedMessages = self.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod, postpone: postpone)
} else {
transformedMessages = self.transformEnqueueMessages(messages)
}
let effectiveSilentPosting = silentPosting ?? self.presentationInterfaceState.interfaceState.silentPosting
let transformedMessages = self.transformEnqueueMessages(messages, silentPosting: effectiveSilentPosting, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod, postpone: postpone)
guard let peerId = self.chatLocation.peerId else {
return
@ -783,7 +771,7 @@ extension ChatControllerImpl {
guard let videoRecorderValue = self.videoRecorderValue else {
return
}
videoRecorderValue.sendVideoRecording(silentPosting: silentPosting, scheduleTime: scheduleTime, messageEffect: messageEffect)
videoRecorderValue.sendVideoRecording(silentPosting: silentPosting, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod, messageEffect: messageEffect)
}
}
}

View file

@ -94,7 +94,7 @@ func updateChatPresentationInterfaceStateImpl(
guard let selfController, value else {
return
}
selfController.present(textAlertController(context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: selfController.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .defaultAction, title: selfController.presentationData.strings.Common_Cancel, action: {
selfController.present(textAlertController(context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: selfController.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .genericAction, title: selfController.presentationData.strings.Common_Cancel, action: {
}), TextAlertAction(type: .defaultAction, title: selfController.presentationData.strings.Common_OK, action: { [weak selfController] in
guard let selfController else {
return
@ -151,7 +151,7 @@ func updateChatPresentationInterfaceStateImpl(
case .generic:
break
case let .inlineBotLocationRequest(peerId):
selfController.present(textAlertController(context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: selfController.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .defaultAction, title: selfController.presentationData.strings.Common_Cancel, action: { [weak selfController] in
selfController.present(textAlertController(context: selfController.context, updatedPresentationData: selfController.updatedPresentationData, title: nil, text: selfController.presentationData.strings.Conversation_ShareInlineBotLocationConfirmation, actions: [TextAlertAction(type: .genericAction, title: selfController.presentationData.strings.Common_Cancel, action: { [weak selfController] in
guard let selfController else {
return
}

View file

@ -2387,9 +2387,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}, shouldAnimateMessageTransition ? correlationId : nil)
strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime, repeatPeriod in
strongSelf.presentScheduleTimePicker(completion: { [weak self] result in
if let strongSelf = self {
let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod, postpone: postpone)
let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: result.silentPosting, scheduleTime: result.time, repeatPeriod: result.repeatPeriod, postpone: postpone)
strongSelf.sendMessages(transformedMessages)
}
})
@ -2557,9 +2557,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
messages = strongSelf.transformEnqueueMessages(messages, silentPosting: true)
strongSelf.sendMessages(messages)
} else if schedule {
strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime, repeatPeriod in
strongSelf.presentScheduleTimePicker(completion: { [weak self] result in
if let strongSelf = self {
let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod)
let transformedMessages = strongSelf.transformEnqueueMessages(messages, silentPosting: result.silentPosting, scheduleTime: result.time, repeatPeriod: result.repeatPeriod)
strongSelf.sendMessages(transformedMessages)
}
})
@ -4031,15 +4031,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard !self.presentAccountFrozenInfoIfNeeded(delay: true) else {
return
}
self.presentScheduleTimePicker(completion: { [weak self] time, repeatPeriod in
self.presentScheduleTimePicker(completion: { [weak self] result in
if let strongSelf = self {
if let _ = strongSelf.presentationInterfaceState.interfaceState.mediaDraftState {
strongSelf.sendMediaRecording(scheduleTime: time, messageEffect: (params?.effect).flatMap {
strongSelf.sendMediaRecording(silentPosting: result.silentPosting, scheduleTime: result.time, repeatPeriod: result.repeatPeriod, messageEffect: (params?.effect).flatMap {
return ChatSendMessageEffect(id: $0.id)
})
} else {
let silentPosting = strongSelf.presentationInterfaceState.interfaceState.silentPosting
strongSelf.chatDisplayNode.sendCurrentMessage(silentPosting: silentPosting, scheduleTime: time, repeatPeriod: repeatPeriod, messageEffect: (params?.effect).flatMap {
strongSelf.chatDisplayNode.sendCurrentMessage(silentPosting: result.silentPosting, scheduleTime: result.time, repeatPeriod: result.repeatPeriod, messageEffect: (params?.effect).flatMap {
return ChatSendMessageEffect(id: $0.id)
}) { [weak self] in
if let strongSelf = self {
@ -4047,7 +4046,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
$0.updatedInterfaceState { $0.withUpdatedReplyMessageSubject(nil).withUpdatedSendMessageEffect(nil).withUpdatedPostSuggestionState(nil).withUpdatedForwardMessageIds(nil).withUpdatedForwardOptionsState(nil).withUpdatedComposeInputState(ChatTextInputState(inputText: NSAttributedString(string: ""))) }
})
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp {
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp {
strongSelf.openScheduledMessages()
}
}
@ -8679,9 +8678,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
}
} else {
self.presentScheduleTimePicker(style: media ? .media : .default, dismissByTapOutside: false, completion: { [weak self] time, repeatPeriod in
self.presentScheduleTimePicker(style: media ? .media : .default, dismissByTapOutside: false, completion: { [weak self] result in
if let strongSelf = self {
strongSelf.sendMessages(strongSelf.transformEnqueueMessages(messages, silentPosting: false, scheduleTime: time, repeatPeriod: repeatPeriod, postpone: postpone), commit: true)
strongSelf.sendMessages(strongSelf.transformEnqueueMessages(messages, silentPosting: result.silentPosting, scheduleTime: result.time, repeatPeriod: result.repeatPeriod, postpone: postpone), commit: true)
}
})
}
@ -8941,7 +8940,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}))
}
func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, resetTextInputState: Bool = true, postpone: Bool = false) {
func enqueueChatContextResult(_ results: ChatContextResultCollection, _ result: ChatContextResult, hideVia: Bool = false, closeMediaInput: Bool = false, silentPosting: Bool = false, scheduleTime: Int32? = nil, resetTextInputState: Bool = true, postpone: Bool = false) {
if !canSendMessagesToChat(self.presentationInterfaceState) {
return
}
@ -8955,7 +8954,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
isScheduledMessages = true
}
let sendMessage: (Int32?) -> Void = { [weak self] scheduleTime in
let sendMessage: (Int32?, Bool) -> Void = { [weak self] scheduleTime, silentPosting in
guard let self else {
return
}
@ -8991,12 +8990,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
}
if isScheduledMessages {
self.presentScheduleTimePicker(style: .default, dismissByTapOutside: false, completion: { time, repeatPeriod in
sendMessage(time)
if let scheduleTime {
sendMessage(scheduleTime, silentPosting)
} else if isScheduledMessages {
self.presentScheduleTimePicker(style: .default, dismissByTapOutside: false, completion: { result in
sendMessage(result.time, result.silentPosting)
})
} else {
sendMessage(nil)
sendMessage(nil, silentPosting)
}
}
@ -10249,6 +10250,12 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
}
func presentScheduleTimePicker(style: ChatScheduleTimeControllerStyle = .default, selectedTime: Int32? = nil, selectedRepeatPeriod: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (Int32, Int32?) -> Void) {
self.presentScheduleTimePicker(style: style, selectedTime: selectedTime, selectedRepeatPeriod: selectedRepeatPeriod, dismissByTapOutside: dismissByTapOutside, completion: { result in
completion(result.time, result.repeatPeriod)
})
}
func presentScheduleTimePicker(style: ChatScheduleTimeControllerStyle = .default, selectedTime: Int32? = nil, selectedRepeatPeriod: Int32? = nil, dismissByTapOutside: Bool = true, completion: @escaping (ChatScheduleTimeScreen.Result) -> Void) {
guard let peerId = self.chatLocation.peerId else {
return
}
@ -10279,9 +10286,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
currentTime: selectedTime,
currentRepeatPeriod: selectedRepeatPeriod,
minimalTime: strongSelf.presentationInterfaceState.slowmodeState?.timeout,
silentPosting: strongSelf.presentationInterfaceState.interfaceState.silentPosting,
isDark: style == .media,
completion: { result in
completion(result.time, result.repeatPeriod)
completion(result)
}
)
strongSelf.chatDisplayNode.dismissInput()

View file

@ -21,7 +21,7 @@ fileprivate struct InitialBannedRights {
}
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 {
return
}
@ -30,7 +30,12 @@ extension ChatControllerImpl {
}
var title: String? = messageIds.count == 1 ? self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTitleSingle : self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTitleMultiple
var title: String?
if let _ = reactionPeerId {
title = self.presentationData.strings.Chat_AdminAction_ToastReactionsDeletedTitleSingle
} else {
title = messageIds.count == 1 ? self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTitleSingle : self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTitleMultiple
}
if !result.deleteAllFromPeers.isEmpty {
title = self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTitleMultiple
}
@ -74,6 +79,10 @@ extension ChatControllerImpl {
let _ = self.context.engine.messages.clearAuthorHistory(peerId: messagesPeerId, memberId: authorId).startStandalone()
}
for authorId in result.deleteAllReactionsFromPeers {
let _ = self.context.engine.messages.deleteAllReactionsWithAuthor(peerId: messagesPeerId, authorId: authorId).startStandalone()
}
for authorId in result.reportSpamPeers {
let _ = self.context.engine.peers.reportPeer(peerId: authorId, reason: .spam, message: "").startStandalone()
}
@ -88,9 +97,17 @@ extension ChatControllerImpl {
}
if text.isEmpty {
text = messageIds.count == 1 ? self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextSingle : self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextMultiple
if !result.deleteAllFromPeers.isEmpty {
if let _ = reactionPeerId {
text = self.presentationData.strings.Chat_AdminAction_ToastReactionsDeletedTextSingle
} else {
text = messageIds.count == 1 ? self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextSingle : self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextMultiple
}
if !result.deleteAllFromPeers.isEmpty && !result.deleteAllReactionsFromPeers.isEmpty {
text = self.presentationData.strings.Chat_AdminAction_ToastMessagesAndReactionsDeletedText
} else if !result.deleteAllFromPeers.isEmpty {
text = self.presentationData.strings.Chat_AdminAction_ToastMessagesDeletedTextMultiple
} else if !result.deleteAllReactionsFromPeers.isEmpty {
text = self.presentationData.strings.Chat_AdminAction_ToastReactionsDeletedTextMultiple
}
title = nil
}
@ -332,7 +349,7 @@ extension ChatControllerImpl {
guard let self else {
return
}
self.applyAdminUserActionsResult(messageIds: messageIds, result: result, initialUserBannedRights: initialUserBannedRights)
self.applyAdminUserActionsResult(messageIds: messageIds, reactionPeerId: authorPeer.id, result: result, initialUserBannedRights: initialUserBannedRights)
})
} else {
mode = .chat(
@ -428,7 +445,7 @@ extension ChatControllerImpl {
}
self.beginDeleteMessagesWithUndo(messageIds: messageIds, type: .forEveryone)
}),
TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {})
TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {})
],
actionLayout: .vertical,
parseMarkdown: true
@ -490,7 +507,7 @@ extension ChatControllerImpl {
}
self.beginDeleteMessagesWithUndo(messageIds: messageIds, type: .forEveryone)
}),
TextAlertAction(type: .defaultAction, title: self.presentationData.strings.Common_Cancel, action: {})
TextAlertAction(type: .genericAction, title: self.presentationData.strings.Common_Cancel, action: {})
],
actionLayout: .vertical,
parseMarkdown: true
@ -568,7 +585,7 @@ extension ChatControllerImpl {
let dateString = stringForDate(timestamp: giveaway.untilDate, timeZone: .current, strings: strongSelf.presentationData.strings)
strongSelf.present(textAlertController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, title: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Title, text: strongSelf.presentationData.strings.Chat_Giveaway_DeleteConfirmation_Text(dateString).string, actions: [TextAlertAction(type: .destructiveAction, title: strongSelf.presentationData.strings.Common_Delete, action: {
commit()
}), TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
}), TextAlertAction(type: .genericAction, title: strongSelf.presentationData.strings.Common_Cancel, action: {
})], parseMarkdown: true), in: .window(.root))
}
f(.default)

View file

@ -317,14 +317,14 @@ extension ChatControllerImpl {
let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: true)
commit(transformedMessages)
case .schedule:
strongSelf.presentScheduleTimePicker(completion: { [weak self] scheduleTime, repeatPeriod in
strongSelf.presentScheduleTimePicker(completion: { [weak self] timeResult in
if let strongSelf = self {
let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: false, scheduleTime: scheduleTime, repeatPeriod: repeatPeriod)
let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: timeResult.silentPosting, scheduleTime: timeResult.time, repeatPeriod: timeResult.repeatPeriod)
commit(transformedMessages)
}
})
case .whenOnline:
let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: false, scheduleTime: scheduleWhenOnlineTimestamp)
let transformedMessages = strongSelf.transformEnqueueMessages(result, silentPosting: strongSelf.presentationInterfaceState.interfaceState.silentPosting, scheduleTime: scheduleWhenOnlineTimestamp)
commit(transformedMessages)
}
}

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))
contactsController.presentScheduleTimePicker = { [weak self] completion in
if let strongSelf = self {
strongSelf.presentScheduleTimePicker(completion: completion)
strongSelf.presentScheduleTimePicker(completion: { result in
completion(result.time, result.repeatPeriod, result.silentPosting)
})
}
}
contactsController.navigationPresentation = .modal
@ -1057,10 +1059,10 @@ extension ChatControllerImpl {
}
}, presentSchedulePicker: { [weak self] _, done in
if let strongSelf = self {
strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time, repeatPeriod in
strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] result in
if let strongSelf = self {
done(time)
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp {
done(result.time, result.silentPosting)
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp {
strongSelf.openScheduledMessages()
}
}
@ -1114,10 +1116,10 @@ extension ChatControllerImpl {
})], actionLayout: .vertical), in: .window(.root))
}, presentSchedulePicker: { [weak self] _, done in
if let strongSelf = self {
strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time, repeatPeriod in
strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] result in
if let strongSelf = self {
done(time)
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp {
done(result.time, result.silentPosting)
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp {
strongSelf.openScheduledMessages()
}
}
@ -1367,10 +1369,10 @@ extension ChatControllerImpl {
}
controller.presentSchedulePicker = { [weak self] media, done in
if let strongSelf = self {
strongSelf.presentScheduleTimePicker(style: media ? .media : .default, completion: { [weak self] time, repeatPeriod in
strongSelf.presentScheduleTimePicker(style: media ? .media : .default, completion: { [weak self] result in
if let strongSelf = self {
done(time)
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp {
done(result.time, result.silentPosting)
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp {
strongSelf.openScheduledMessages()
}
}
@ -1495,20 +1497,20 @@ extension ChatControllerImpl {
configureLegacyAssetPicker(controller, context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, initialCaption: inputText, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, presentWebSearch: editingMedia ? nil : { [weak self, weak legacyController] in
if let strongSelf = self {
let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: searchBotsConfiguration, mode: .media(attachment: false, completion: { results, selectionState, editingState, silentPosting in
let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: searchBotsConfiguration, mode: .media(attachment: false, completion: { results, selectionState, editingState, silentPosting, scheduleTime in
if let legacyController = legacyController {
legacyController.dismiss()
}
legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { result in
if let strongSelf = self {
strongSelf.enqueueChatContextResult(results, result, hideVia: true)
strongSelf.enqueueChatContextResult(results, result, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime)
}
}, enqueueMediaMessages: { signals in
if let strongSelf = self {
if editingMedia {
strongSelf.editMessageMediaWithLegacySignals(signals)
} else {
strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting)
strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime)
}
}
})
@ -1533,10 +1535,10 @@ extension ChatControllerImpl {
strongSelf.present(textAlertController(context: strongSelf.context, title: nil, text: text, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), in: .window(.root))
}, presentSchedulePicker: { [weak self] media, done in
if let strongSelf = self {
strongSelf.presentScheduleTimePicker(style: media ? .media : .default, completion: { [weak self] time, repeatPeriod in
strongSelf.presentScheduleTimePicker(style: media ? .media : .default, completion: { [weak self] result in
if let strongSelf = self {
done(time)
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp {
done(result.time, result.silentPosting)
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp {
strongSelf.openScheduledMessages()
}
}
@ -1578,18 +1580,18 @@ extension ChatControllerImpl {
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Configuration.SearchBots())
|> deliverOnMainQueue).startStandalone(next: { [weak self] configuration in
if let strongSelf = self {
let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: configuration, mode: .media(attachment: attachment, completion: { [weak self] results, selectionState, editingState, silentPosting in
let controller = WebSearchController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: EnginePeer(peer), chatLocation: strongSelf.chatLocation, configuration: configuration, mode: .media(attachment: attachment, completion: { [weak self] results, selectionState, editingState, silentPosting, scheduleTime in
self?.attachmentController?.dismiss(animated: true, completion: nil)
legacyEnqueueWebSearchMessages(selectionState, editingState, enqueueChatContextResult: { [weak self] result in
if let strongSelf = self {
strongSelf.enqueueChatContextResult(results, result, hideVia: true)
strongSelf.enqueueChatContextResult(results, result, hideVia: true, silentPosting: silentPosting, scheduleTime: scheduleTime)
}
}, enqueueMediaMessages: { [weak self] signals in
if let strongSelf = self, !signals.isEmpty {
if editingMessage {
strongSelf.editMessageMediaWithLegacySignals(signals)
} else {
strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting)
strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime)
}
}
})
@ -1967,10 +1969,10 @@ extension ChatControllerImpl {
}
}, presentSchedulePicker: { [weak self] _, done in
if let strongSelf = self {
strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] time, repeatPeriod in
strongSelf.presentScheduleTimePicker(style: .media, completion: { [weak self] result in
if let strongSelf = self {
done(time)
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && time != scheduleWhenOnlineTimestamp {
done(result.time, result.silentPosting)
if strongSelf.presentationInterfaceState.subject != .scheduledMessages && result.time != scheduleWhenOnlineTimestamp {
strongSelf.openScheduledMessages()
}
}

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? {
guard let controllerInteraction else {
return nil
@ -45,15 +56,8 @@ func textInputContextPanel(context: AccountContext, chatPresentationInterfaceSta
}).first else {
return nil
}
var hasBannedInlineContent = false
if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.hasBannedPermission(.banSendInline) != nil {
hasBannedInlineContent = true
} else if let group = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramGroup, group.hasBannedPermission(.banSendInline) {
hasBannedInlineContent = true
}
if hasBannedInlineContent {
if hasBannedInlineContent(chatPresentationInterfaceState: chatPresentationInterfaceState) {
switch inputQueryResult {
case .stickers, .contextRequestResult:
if let currentPanel = currentPanel as? DisabledContextResultsChatInputContextPanelNode {
@ -183,15 +187,8 @@ func inputContextPanelForChatPresentationIntefaceState(_ chatPresentationInterfa
}).first else {
return nil
}
var hasBannedInlineContent = false
if let channel = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramChannel, channel.hasBannedPermission(.banSendInline) != nil {
hasBannedInlineContent = true
} else if let group = chatPresentationInterfaceState.renderedPeer?.peer as? TelegramGroup, group.hasBannedPermission(.banSendInline) {
hasBannedInlineContent = true
}
if hasBannedInlineContent {
if hasBannedInlineContent(chatPresentationInterfaceState: chatPresentationInterfaceState) {
switch inputQueryResult {
case .stickers, .contextRequestResult:
if let currentPanel = currentPanel as? DisabledContextResultsChatInputContextPanelNode {
@ -305,4 +302,3 @@ func chatOverlayContextPanelForChatPresentationIntefaceState(_ chatPresentationI
return nil
}

View file

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

View file

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

View file

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

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.separatorNode.backgroundColor = interfaceState.theme.list.itemPlainSeparatorColor
guard let (untilDate, personal) = (interfaceState.renderedPeer?.peer as? TelegramChannel)?.hasBannedPermission(.banSendInline) else {
let canBypass = canBypassRestrictions(chatPresentationInterfaceState: interfaceState)
guard let (untilDate, personal) = (interfaceState.renderedPeer?.peer as? TelegramChannel)?.hasBannedPermission(.banSendInline, ignoreDefault: canBypass) else {
return
}
let banDescription: String

View file

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