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