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