mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-10-09 03:20:48 +00:00
Merge commit '38f6adeda1758d0f605000bc4356688c48768c0b'
This commit is contained in:
commit
16a1ac35db
@ -1054,7 +1054,12 @@ private final class NotificationServiceHandler {
|
||||
}
|
||||
|
||||
if let category = aps["category"] as? String {
|
||||
content.category = category
|
||||
if peerId.isGroupOrChannel && ["r", "m"].contains(category) {
|
||||
content.category = "g\(category)"
|
||||
} else {
|
||||
content.category = category
|
||||
}
|
||||
|
||||
|
||||
let _ = messageId
|
||||
|
||||
|
BIN
Telegram/Telegram-iOS/Resources/ForumAvatarMask.tgs
Normal file
BIN
Telegram/Telegram-iOS/Resources/ForumAvatarMask.tgs
Normal file
Binary file not shown.
Binary file not shown.
@ -9090,3 +9090,5 @@ Sorry for the inconvenience.";
|
||||
"PeerInfo.CancelSelectionAlertNo" = "No";
|
||||
|
||||
"StickerPacksSettings.SuggestAnimatedEmojiInfo" = "Each time you enter an emoji you can replace it with an animated emoji.";
|
||||
|
||||
"DialogList.DeleteBotClearHistory" = "Clear Chat History";
|
||||
|
@ -13,6 +13,7 @@ swift_library(
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/rlottie:RLottieBinding",
|
||||
"//submodules/lottie-ios:Lottie",
|
||||
"//submodules/GZip:GZip",
|
||||
"//submodules/AppBundle:AppBundle",
|
||||
"//submodules/Display:Display",
|
||||
],
|
||||
|
@ -2,6 +2,7 @@ import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Lottie
|
||||
import GZip
|
||||
import AppBundle
|
||||
import Display
|
||||
|
||||
@ -32,7 +33,15 @@ public final class AnimationNode: ASDisplayNode {
|
||||
super.init()
|
||||
|
||||
self.setViewBlock({
|
||||
if let animationName = animationName, let url = getAppBundle().url(forResource: animationName, withExtension: "json"), let animation = Animation.filepath(url.path) {
|
||||
var animation: Animation?
|
||||
if let animationName {
|
||||
if let url = getAppBundle().url(forResource: animationName, withExtension: "json"), let maybeAnimation = Animation.filepath(url.path) {
|
||||
animation = maybeAnimation
|
||||
} else if let url = getAppBundle().url(forResource: animationName, withExtension: "tgs"), let data = try? Data(contentsOf: URL(fileURLWithPath: url.path)), let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) {
|
||||
animation = try? Animation.from(data: unpackedData, strategy: .codable)
|
||||
}
|
||||
}
|
||||
if let animation {
|
||||
let view = AnimationView(animation: animation, configuration: LottieConfiguration(renderingEngine: .mainThread, decodingStrategy: .codable))
|
||||
view.animationSpeed = self.speed
|
||||
view.backgroundColor = .clear
|
||||
@ -104,6 +113,10 @@ public final class AnimationNode: ASDisplayNode {
|
||||
self.animationView()?.currentProgress = 1.0
|
||||
}
|
||||
|
||||
public func setProgress(_ progress: CGFloat) {
|
||||
self.animationView()?.currentProgress = progress
|
||||
}
|
||||
|
||||
public func setAnimation(name: String, colors: [String: UIColor]? = nil) {
|
||||
self.currentParams = (name, colors)
|
||||
if let url = getAppBundle().url(forResource: name, withExtension: "json"), let animation = Animation.filepath(url.path) {
|
||||
|
@ -36,6 +36,7 @@ swift_library(
|
||||
"//submodules/Components/AnimatedStickerComponent:AnimatedStickerComponent",
|
||||
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
|
||||
"//submodules/ShimmerEffect:ShimmerEffect",
|
||||
"//submodules/TextFormat:TextFormat",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -18,6 +18,7 @@ import SemanticStatusNode
|
||||
import MediaResources
|
||||
import MultilineTextComponent
|
||||
import ShimmerEffect
|
||||
import TextFormat
|
||||
|
||||
private let buttonSize = CGSize(width: 88.0, height: 49.0)
|
||||
private let smallButtonWidth: CGFloat = 69.0
|
||||
@ -885,20 +886,29 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
|
||||
}, openLinkEditing: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
var selectionRange: Range<Int>?
|
||||
var text: String?
|
||||
var text: NSAttributedString?
|
||||
var inputMode: ChatInputMode?
|
||||
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, { state in
|
||||
selectionRange = state.interfaceState.effectiveInputState.selectionRange
|
||||
if let selectionRange = selectionRange {
|
||||
text = state.interfaceState.effectiveInputState.inputText.attributedSubstring(from: NSRange(location: selectionRange.startIndex, length: selectionRange.count)).string
|
||||
text = state.interfaceState.effectiveInputState.inputText.attributedSubstring(from: NSRange(location: selectionRange.startIndex, length: selectionRange.count))
|
||||
}
|
||||
inputMode = state.inputMode
|
||||
return state
|
||||
})
|
||||
|
||||
var link: String?
|
||||
if let text {
|
||||
text.enumerateAttributes(in: NSMakeRange(0, text.length)) { attributes, _, _ in
|
||||
if let linkAttribute = attributes[ChatTextInputAttributes.textUrl] as? ChatTextInputTextUrlAttribute {
|
||||
link = linkAttribute.url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let controller = chatTextLinkEditController(sharedContext: strongSelf.context.sharedContext, updatedPresentationData: (presentationData, .never()), account: strongSelf.context.account, text: text ?? "", link: nil, apply: { [weak self] link in
|
||||
let controller = chatTextLinkEditController(sharedContext: strongSelf.context.sharedContext, updatedPresentationData: (presentationData, .never()), account: strongSelf.context.account, text: text?.string ?? "", link: link, apply: { [weak self] link in
|
||||
if let strongSelf = self, let inputMode = inputMode, let selectionRange = selectionRange {
|
||||
if let link = link {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, { state in
|
||||
|
@ -252,6 +252,9 @@ public final class AuthorizationSequencePhoneEntryController: ViewController, MF
|
||||
}
|
||||
|
||||
@objc func nextPressed() {
|
||||
guard self.confirmationController == nil else {
|
||||
return
|
||||
}
|
||||
let (_, _, number) = self.controllerNode.codeAndNumber
|
||||
if !number.isEmpty {
|
||||
let logInNumber = cleanPhoneNumber(self.controllerNode.currentNumber, removePlus: true)
|
||||
|
@ -759,6 +759,7 @@ final class PhoneConfirmationController: ViewController {
|
||||
|
||||
private let codeTargetNode: ImmediateTextNode
|
||||
private let phoneTargetNode: ImmediateTextNode
|
||||
private let measureTargetNode: ImmediateTextNode
|
||||
|
||||
private let textNode: ImmediateTextNode
|
||||
private let textActivateAreaNode: AccessibilityAreaNode
|
||||
@ -824,6 +825,10 @@ final class PhoneConfirmationController: ViewController {
|
||||
self.phoneTargetNode = ImmediateTextNode()
|
||||
self.phoneTargetNode.displaysAsynchronously = false
|
||||
|
||||
self.measureTargetNode = ImmediateTextNode()
|
||||
self.measureTargetNode.displaysAsynchronously = false
|
||||
self.measureTargetNode.maximumNumberOfLines = 1
|
||||
|
||||
let targetString = NSMutableAttributedString(string: number, font: largeFont, textColor: theme.list.itemPrimaryTextColor)
|
||||
targetString.addAttribute(NSAttributedString.Key.kern, value: 1.6, range: NSRange(location: 0, length: sourceString.length))
|
||||
self.phoneTargetNode.attributedText = targetString
|
||||
@ -1015,6 +1020,12 @@ final class PhoneConfirmationController: ViewController {
|
||||
fontSize = 30.0
|
||||
}
|
||||
|
||||
self.measureTargetNode.attributedText = NSAttributedString(string: self.code + " " + self.number, font: Font.with(size: fontSize, design: .regular, weight: .bold, traits: [.monospacedNumbers]), textColor: self.theme.list.itemPrimaryTextColor)
|
||||
let measuredSize = self.measureTargetNode.updateLayout(CGSize(width: 1000.0, height: .greatestFiniteMagnitude))
|
||||
if measuredSize.width > maxWidth {
|
||||
fontSize = floor(0.8 * fontSize)
|
||||
}
|
||||
|
||||
let largeFont = Font.with(size: fontSize, design: .regular, weight: .bold, traits: [.monospacedNumbers])
|
||||
|
||||
self.codeTargetNode.attributedText = NSAttributedString(string: self.code, font: largeFont, textColor: self.theme.list.itemPrimaryTextColor)
|
||||
|
@ -26,9 +26,8 @@ final class BrowserWebContent: UIView, BrowserContent, UIScrollViewDelegate {
|
||||
let configuration = WKWebViewConfiguration()
|
||||
|
||||
self.webView = WKWebView(frame: CGRect(), configuration: configuration)
|
||||
if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
|
||||
self.webView.allowsLinkPreview = false
|
||||
}
|
||||
self.webView.allowsLinkPreview = false
|
||||
|
||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||
self.webView.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
@ -690,13 +690,13 @@ final class CallListControllerNode: ASDisplayNode {
|
||||
let alpha: CGFloat = isHidden ? 0.0 : 1.0
|
||||
let previousAlpha = self.emptyTextNode.alpha
|
||||
self.emptyTextNode.alpha = alpha
|
||||
self.emptyTextNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.2)
|
||||
self.emptyTextNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.25)
|
||||
|
||||
if previousAlpha.isZero && !alpha.isZero {
|
||||
self.emptyAnimationNode.visibility = true
|
||||
}
|
||||
self.emptyAnimationNode.alpha = alpha
|
||||
self.emptyAnimationNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.2, completion: { [weak self] _ in
|
||||
self.emptyAnimationNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.25, completion: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
if !previousAlpha.isZero && strongSelf.emptyAnimationNode.alpha.isZero {
|
||||
strongSelf.emptyAnimationNode.visibility = false
|
||||
@ -705,9 +705,9 @@ final class CallListControllerNode: ASDisplayNode {
|
||||
})
|
||||
|
||||
self.emptyButtonIconNode.alpha = alpha
|
||||
self.emptyButtonIconNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.2)
|
||||
self.emptyButtonIconNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.25)
|
||||
self.emptyButtonTextNode.alpha = alpha
|
||||
self.emptyButtonTextNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.2)
|
||||
self.emptyButtonTextNode.layer.animateAlpha(from: previousAlpha, to: alpha, duration: 0.25)
|
||||
self.emptyButtonNode.isUserInteractionEnabled = !isHidden
|
||||
|
||||
if !isHidden {
|
||||
@ -733,7 +733,6 @@ final class CallListControllerNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
self.emptyTextNode.attributedText = NSAttributedString(string: emptyText, font: textFont, textColor: color, paragraphAlignment: .center)
|
||||
|
||||
self.emptyButtonTextNode.attributedText = NSAttributedString(string: buttonText, font: buttonFont, textColor: theme.list.itemAccentColor, paragraphAlignment: .center)
|
||||
|
||||
if let layout = self.containerLayout {
|
||||
|
@ -230,7 +230,7 @@ func countMeaningfulCallListEntries(_ entries: [CallListNodeEntry]) -> Int {
|
||||
var count: Int = 0
|
||||
for entry in entries {
|
||||
switch entry.stableId {
|
||||
case .setting, .groupCall:
|
||||
case .setting:
|
||||
break
|
||||
default:
|
||||
count += 1
|
||||
|
@ -1206,10 +1206,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
let controller = ForumCreateTopicScreen(context: context, peerId: peerId, mode: .create)
|
||||
controller.navigationPresentation = .modal
|
||||
|
||||
controller.completion = { [weak controller] title, fileId, _ in
|
||||
controller.completion = { [weak controller] title, fileId, iconColor, _ in
|
||||
controller?.isInProgress = true
|
||||
|
||||
let _ = (context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconColor: ForumCreateTopicScreen.iconColors.randomElement()!, iconFileId: fileId)
|
||||
let _ = (context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconColor: iconColor, iconFileId: fileId)
|
||||
|> deliverOnMainQueue).start(next: { topicId in
|
||||
let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: topicId, messageId: nil, navigationController: navigationController, activateInput: .text, keepStack: .never).start()
|
||||
}, error: { _ in
|
||||
@ -2464,10 +2464,10 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
let controller = ForumCreateTopicScreen(context: context, peerId: peerId, mode: .create)
|
||||
controller.navigationPresentation = .modal
|
||||
|
||||
controller.completion = { [weak controller] title, fileId, _ in
|
||||
controller.completion = { [weak controller] title, fileId, iconColor, _ in
|
||||
controller?.isInProgress = true
|
||||
|
||||
let _ = (context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconColor: ForumCreateTopicScreen.iconColors.randomElement()!, iconFileId: fileId)
|
||||
let _ = (context.engine.peers.createForumChannelTopic(id: peerId, title: title, iconColor: iconColor, iconFileId: fileId)
|
||||
|> deliverOnMainQueue).start(next: { topicId in
|
||||
if let navigationController = (sourceController.navigationController as? NavigationController) {
|
||||
let _ = context.sharedContext.navigateToForumThread(context: context, peerId: peerId, threadId: topicId, messageId: nil, navigationController: navigationController, activateInput: .text, keepStack: .never).start()
|
||||
@ -3413,7 +3413,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
} else if case let .user(user) = chatPeer, user.botInfo != nil {
|
||||
canStop = !user.flags.contains(.isSupport)
|
||||
canClear = user.botInfo == nil
|
||||
deleteTitle = strongSelf.presentationData.strings.ChatList_DeleteChat
|
||||
} else if case .secretChat = chatPeer {
|
||||
canClear = true
|
||||
@ -3478,6 +3477,22 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
} else {
|
||||
items.append(DeleteChatPeerActionSheetItem(context: strongSelf.context, peer: mainPeer, chatPeer: chatPeer, action: .delete, strings: strongSelf.presentationData.strings, nameDisplayOrder: strongSelf.presentationData.nameDisplayOrder))
|
||||
|
||||
if canStop {
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.DialogList_DeleteBotConversationConfirmation, color: .destructive, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
|
||||
if let strongSelf = self {
|
||||
strongSelf.maybeAskForPeerChatRemoval(peer: peer, completion: { _ in
|
||||
}, removed: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.peerId, isBlocked: true).start()
|
||||
})
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
if canClear {
|
||||
let beginClear: (InteractiveHistoryClearingType) -> Void = { type in
|
||||
guard let strongSelf = self else {
|
||||
@ -3523,7 +3538,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}), in: .current)
|
||||
}
|
||||
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.DialogList_ClearHistoryConfirmation, color: .accent, action: { [weak actionSheet] in
|
||||
items.append(ActionSheetButtonItem(title: canStop ? strongSelf.presentationData.strings.DialogList_DeleteBotClearHistory : strongSelf.presentationData.strings.DialogList_ClearHistoryConfirmation, color: .accent, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
|
||||
guard let strongSelf = self else {
|
||||
@ -3586,7 +3601,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
strongSelf.schedulePeerChatRemoval(peer: peer, type: .forEveryone, deleteGloballyIfPossible: true, completion: {
|
||||
})
|
||||
}))
|
||||
} else {
|
||||
} else if !canStop {
|
||||
items.append(ActionSheetButtonItem(title: deleteTitle, color: .destructive, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
guard let strongSelf = self else {
|
||||
@ -3658,23 +3673,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
if canStop {
|
||||
items.append(ActionSheetButtonItem(title: strongSelf.presentationData.strings.DialogList_DeleteBotConversationConfirmation, color: .destructive, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
|
||||
if let strongSelf = self {
|
||||
strongSelf.maybeAskForPeerChatRemoval(peer: peer, completion: { _ in
|
||||
}, removed: {
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
let _ = strongSelf.context.engine.privacy.requestUpdatePeerIsBlocked(peerId: peer.peerId, isBlocked: true).start()
|
||||
})
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
actionSheet.setItemGroups([ActionSheetItemGroup(items: items),
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
|
@ -127,10 +127,8 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
||||
case _ as TelegramMediaImage:
|
||||
if message.text.isEmpty {
|
||||
messageText = strings.Message_Photo
|
||||
} else if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
|
||||
if enableMediaEmoji {
|
||||
messageText = "🖼 \(messageText)"
|
||||
}
|
||||
} else if enableMediaEmoji {
|
||||
messageText = "🖼 \(messageText)"
|
||||
}
|
||||
case let fileMedia as TelegramMediaFile:
|
||||
var processed = false
|
||||
@ -188,7 +186,7 @@ public func chatListItemStrings(strings: PresentationStrings, nameDisplayOrder:
|
||||
if message.text.isEmpty {
|
||||
messageText = strings.Message_Video
|
||||
processed = true
|
||||
} else if #available(iOSApplicationExtension 9.0, iOS 9.0, *) {
|
||||
} else {
|
||||
if enableMediaEmoji {
|
||||
if !fileMedia.isAnimated {
|
||||
messageText = "📹 \(messageText)"
|
||||
|
@ -209,15 +209,6 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
self.cancel = cancel
|
||||
|
||||
self.effectView = UIVisualEffectView()
|
||||
if #available(iOS 9.0, *) {
|
||||
} else {
|
||||
if self.presentationData.theme.rootController.keyboardColor == .dark {
|
||||
self.effectView.effect = UIBlurEffect(style: .dark)
|
||||
} else {
|
||||
self.effectView.effect = UIBlurEffect(style: .light)
|
||||
}
|
||||
self.effectView.alpha = 0.0
|
||||
}
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.dimNode.alpha = 1.0
|
||||
@ -430,14 +421,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
}
|
||||
self.presentationData = presentationData
|
||||
|
||||
if #available(iOS 9.0, *) {
|
||||
} else {
|
||||
if self.presentationData.theme.rootController.keyboardColor == .dark {
|
||||
self.effectView.effect = UIBlurEffect(style: .dark)
|
||||
} else {
|
||||
self.effectView.effect = UIBlurEffect(style: .light)
|
||||
}
|
||||
}
|
||||
self.effectView.effect = makeCustomZoomBlurEffect(isLight: self.presentationData.theme.rootController.keyboardColor == .light)
|
||||
|
||||
self.dimNode.backgroundColor = presentationData.theme.contextMenu.dimColor
|
||||
|
||||
@ -465,11 +449,7 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
self.textInputNode.textView.setContentOffset(self.textInputNode.textView.contentOffset, animated: false)
|
||||
|
||||
UIView.animate(withDuration: 0.2, animations: {
|
||||
if #available(iOS 9.0, *) {
|
||||
self.effectView.effect = makeCustomZoomBlurEffect(isLight: !self.presentationData.theme.overallDarkAppearance)
|
||||
} else {
|
||||
self.effectView.alpha = 1.0
|
||||
}
|
||||
self.effectView.effect = makeCustomZoomBlurEffect(isLight: self.presentationData.theme.rootController.keyboardColor == .light)
|
||||
}, completion: { _ in })
|
||||
self.dimNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
self.contentContainerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
@ -562,12 +542,8 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
}
|
||||
}
|
||||
|
||||
UIView.animate(withDuration: 0.4, animations: {
|
||||
if #available(iOS 9.0, *) {
|
||||
self.effectView.effect = nil
|
||||
} else {
|
||||
self.effectView.alpha = 0.0
|
||||
}
|
||||
UIView.animate(withDuration: 0.2, animations: {
|
||||
self.effectView.effect = nil
|
||||
}, completion: { _ in
|
||||
completedEffect = true
|
||||
intermediateCompletion()
|
||||
@ -596,7 +572,6 @@ final class ChatSendMessageActionSheetControllerNode: ViewControllerTracingNode,
|
||||
}
|
||||
|
||||
let duration = 0.4
|
||||
|
||||
self.sendButtonNode.layer.animatePosition(from: self.sendButtonNode.position, to: self.sendButtonFrame.center, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
||||
completedButton = true
|
||||
intermediateCompletion()
|
||||
|
@ -12,7 +12,7 @@ import UrlEscaping
|
||||
private final class ChatTextLinkEditInputFieldNode: ASDisplayNode, ASEditableTextNodeDelegate {
|
||||
private var theme: PresentationTheme
|
||||
private let backgroundNode: ASImageNode
|
||||
private let textInputNode: EditableTextNode
|
||||
fileprivate let textInputNode: EditableTextNode
|
||||
private let placeholderNode: ASTextNode
|
||||
|
||||
var updateHeight: (() -> Void)?
|
||||
@ -238,6 +238,21 @@ private final class ChatTextLinkEditAlertContentNode: AlertContentNode {
|
||||
}
|
||||
|
||||
self.updateTheme(theme)
|
||||
|
||||
if (link ?? "").isEmpty {
|
||||
Queue.mainQueue().after(0.1, {
|
||||
let pasteboard = UIPasteboard.general
|
||||
if pasteboard.hasURLs {
|
||||
if let url = pasteboard.url?.absoluteString, !url.isEmpty {
|
||||
self.inputFieldNode.text = url
|
||||
if let lastNode = self.actionNodes.last {
|
||||
lastNode.actionEnabled = true
|
||||
}
|
||||
self.inputFieldNode.textInputNode.textView.selectAll(nil)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
|
@ -20,6 +20,11 @@ open class ViewControllerComponentContainer: ViewController {
|
||||
case `default`
|
||||
}
|
||||
|
||||
public enum PresentationMode {
|
||||
case `default`
|
||||
case modal
|
||||
}
|
||||
|
||||
public final class Environment: Equatable {
|
||||
public let statusBarHeight: CGFloat
|
||||
public let navigationHeight: CGFloat
|
||||
@ -139,11 +144,6 @@ open class ViewControllerComponentContainer: ViewController {
|
||||
func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) {
|
||||
self.currentLayout = (layout, navigationHeight)
|
||||
|
||||
var theme = self.theme ?? self.presentationData.theme
|
||||
if theme.list.blocksBackgroundColor.rgb == theme.list.plainBackgroundColor.rgb {
|
||||
theme = theme.withModalBlocksBackground()
|
||||
}
|
||||
|
||||
let environment = ViewControllerComponentContainer.Environment(
|
||||
statusBarHeight: layout.statusBarHeight ?? 0.0,
|
||||
navigationHeight: navigationHeight,
|
||||
@ -152,7 +152,7 @@ open class ViewControllerComponentContainer: ViewController {
|
||||
metrics: layout.metrics,
|
||||
deviceMetrics: layout.deviceMetrics,
|
||||
isVisible: self.currentIsVisible,
|
||||
theme: theme,
|
||||
theme: self.theme ?? self.presentationData.theme,
|
||||
strings: self.presentationData.strings,
|
||||
dateTimeFormat: self.presentationData.dateTimeFormat,
|
||||
controller: { [weak self] in
|
||||
@ -203,7 +203,7 @@ open class ViewControllerComponentContainer: ViewController {
|
||||
private var presentationDataDisposable: Disposable?
|
||||
public private(set) var validLayout: ContainerViewLayout?
|
||||
|
||||
public init<C: Component>(context: AccountContext, component: C, navigationBarAppearance: NavigationBarAppearance, statusBarStyle: StatusBarStyle = .default, theme: PresentationTheme? = nil) where C.EnvironmentType == ViewControllerComponentContainer.Environment {
|
||||
public init<C: Component>(context: AccountContext, component: C, navigationBarAppearance: NavigationBarAppearance, statusBarStyle: StatusBarStyle = .default, presentationMode: PresentationMode = .default, theme: PresentationTheme? = nil) where C.EnvironmentType == ViewControllerComponentContainer.Environment {
|
||||
self.context = context
|
||||
self.component = AnyComponent(component)
|
||||
self.theme = theme
|
||||
@ -224,7 +224,12 @@ open class ViewControllerComponentContainer: ViewController {
|
||||
self.presentationDataDisposable = (self.context.sharedContext.presentationData
|
||||
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
|
||||
if let strongSelf = self {
|
||||
strongSelf.node.presentationData = presentationData
|
||||
var theme = presentationData.theme
|
||||
if case .modal = presentationMode, theme.list.blocksBackgroundColor.rgb == theme.list.plainBackgroundColor.rgb {
|
||||
theme = theme.withModalBlocksBackground()
|
||||
}
|
||||
|
||||
strongSelf.node.presentationData = presentationData.withUpdated(theme: theme)
|
||||
|
||||
switch statusBarStyle {
|
||||
case .none:
|
||||
|
@ -1860,6 +1860,10 @@ final class ControlledTransitionProperty {
|
||||
let toValue: AnyValue
|
||||
private let completion: ((Bool) -> Void)?
|
||||
|
||||
private lazy var animationKey: String = {
|
||||
return "MyCustomAnimation_\(Unmanaged.passUnretained(self).toOpaque())"
|
||||
}()
|
||||
|
||||
init<T: Equatable>(layer: CALayer, path: String, fromValue: T, toValue: T, completion: ((Bool) -> Void)?) where T: AnyValueProviding {
|
||||
self.layer = layer
|
||||
self.path = path
|
||||
@ -1871,7 +1875,7 @@ final class ControlledTransitionProperty {
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.layer.removeAnimation(forKey: "MyCustomAnimation_\(Unmanaged.passUnretained(self).toOpaque())")
|
||||
self.layer.removeAnimation(forKey: self.animationKey)
|
||||
}
|
||||
|
||||
func update(at fraction: CGFloat) {
|
||||
@ -1887,7 +1891,7 @@ final class ControlledTransitionProperty {
|
||||
animation.toValue = value.nsValue
|
||||
animation.timingFunction = CAMediaTimingFunction(name: .linear)
|
||||
animation.isRemovedOnCompletion = false
|
||||
self.layer.add(animation, forKey: "MyCustomAnimation_\(Unmanaged.passUnretained(self).toOpaque())")
|
||||
self.layer.add(animation, forKey: self.animationKey)
|
||||
}
|
||||
|
||||
func complete(atEnd: Bool) {
|
||||
|
@ -82,6 +82,7 @@ public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelega
|
||||
public private(set) var isReady: Bool = false
|
||||
public var isReadyUpdated: (() -> Void)?
|
||||
public var controllerRemoved: (ViewController) -> Void
|
||||
public var requestFilterController: (ViewController) -> Void = { _ in }
|
||||
public var keyboardViewManager: KeyboardViewManager? {
|
||||
didSet {
|
||||
}
|
||||
@ -118,6 +119,8 @@ public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelega
|
||||
var statusBarStyle: StatusBarStyle = .Ignore
|
||||
var statusBarStyleUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
||||
|
||||
|
||||
|
||||
private var panRecognizer: InteractiveTransitionGestureRecognizer?
|
||||
|
||||
public init(isFlat: Bool, controllerRemoved: @escaping (ViewController) -> Void) {
|
||||
@ -211,7 +214,10 @@ public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelega
|
||||
let topController = self.controllers[self.controllers.count - 1]
|
||||
let bottomController = self.controllers[self.controllers.count - 2]
|
||||
|
||||
if !topController.attemptNavigation({
|
||||
if !topController.attemptNavigation({ [weak self, weak topController] in
|
||||
if let self, let topController {
|
||||
self.requestFilterController(topController)
|
||||
}
|
||||
}) {
|
||||
return
|
||||
}
|
||||
|
@ -826,6 +826,9 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
let flatContainer = NavigationContainer(isFlat: self.isFlat, controllerRemoved: { [weak self] controller in
|
||||
self?.controllerRemoved(controller)
|
||||
})
|
||||
flatContainer.requestFilterController = { [weak self] controller in
|
||||
self?.filterController(controller, animated: true)
|
||||
}
|
||||
flatContainer.statusBarStyleUpdated = { [weak self] transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
@ -853,6 +856,9 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
let flatContainer = NavigationContainer(isFlat: self.isFlat, controllerRemoved: { [weak self] controller in
|
||||
self?.controllerRemoved(controller)
|
||||
})
|
||||
flatContainer.requestFilterController = { [weak self] controller in
|
||||
self?.filterController(controller, animated: true)
|
||||
}
|
||||
flatContainer.statusBarStyleUpdated = { [weak self] transition in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
|
@ -156,7 +156,7 @@ final class LocationSearchContainerNode: ASDisplayNode {
|
||||
let searchItems = self.searchQuery.get()
|
||||
|> mapToSignal { query -> Signal<String?, NoError> in
|
||||
if let query = query, !query.isEmpty {
|
||||
return (.complete() |> delay(0.6, queue: Queue.mainQueue()))
|
||||
return (.complete() |> delay(1.0, queue: Queue.mainQueue()))
|
||||
|> then(.single(query))
|
||||
} else {
|
||||
return .single(query)
|
||||
|
@ -3,20 +3,28 @@ import UIKit
|
||||
import Photos
|
||||
import SwiftSignalKit
|
||||
|
||||
private let imageManager = PHCachingImageManager()
|
||||
private let imageManager: PHCachingImageManager = {
|
||||
let imageManager = PHCachingImageManager()
|
||||
imageManager.allowsCachingHighQualityImages = false
|
||||
return imageManager
|
||||
}()
|
||||
|
||||
|
||||
private let assetsQueue = Queue()
|
||||
|
||||
func assetImage(fetchResult: PHFetchResult<PHAsset>, index: Int, targetSize: CGSize, exact: Bool) -> Signal<UIImage?, NoError> {
|
||||
func assetImage(fetchResult: PHFetchResult<PHAsset>, index: Int, targetSize: CGSize, exact: Bool, deliveryMode: PHImageRequestOptionsDeliveryMode = .opportunistic, synchronous: Bool = false) -> Signal<UIImage?, NoError> {
|
||||
let asset = fetchResult[index]
|
||||
return assetImage(asset: asset, targetSize: targetSize, exact: exact)
|
||||
return assetImage(asset: asset, targetSize: targetSize, exact: exact, deliveryMode: deliveryMode, synchronous: synchronous)
|
||||
}
|
||||
|
||||
func assetImage(asset: PHAsset, targetSize: CGSize, exact: Bool) -> Signal<UIImage?, NoError> {
|
||||
func assetImage(asset: PHAsset, targetSize: CGSize, exact: Bool, deliveryMode: PHImageRequestOptionsDeliveryMode = .opportunistic, synchronous: Bool = false) -> Signal<UIImage?, NoError> {
|
||||
return Signal { subscriber in
|
||||
let options = PHImageRequestOptions()
|
||||
options.deliveryMode = deliveryMode
|
||||
if exact {
|
||||
options.resizeMode = .exact
|
||||
}
|
||||
options.isSynchronous = synchronous
|
||||
let token = imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: options) { (image, info) in
|
||||
var degraded = false
|
||||
|
||||
@ -31,17 +39,15 @@ func assetImage(asset: PHAsset, targetSize: CGSize, exact: Bool) -> Signal<UIIma
|
||||
|
||||
if let image = image {
|
||||
subscriber.putNext(image)
|
||||
if !degraded {
|
||||
if !degraded || deliveryMode == .fastFormat {
|
||||
subscriber.putCompletion()
|
||||
}
|
||||
}
|
||||
}
|
||||
return ActionDisposable {
|
||||
assetsQueue.async {
|
||||
imageManager.cancelImageRequest(token)
|
||||
}
|
||||
imageManager.cancelImageRequest(token)
|
||||
}
|
||||
} |> runOn(assetsQueue)
|
||||
}
|
||||
}
|
||||
|
||||
func assetVideo(fetchResult: PHFetchResult<PHAsset>, index: Int) -> Signal<AVAsset?, NoError> {
|
||||
@ -49,7 +55,6 @@ func assetVideo(fetchResult: PHFetchResult<PHAsset>, index: Int) -> Signal<AVAss
|
||||
let asset = fetchResult[index]
|
||||
|
||||
let options = PHVideoRequestOptions()
|
||||
|
||||
let token = imageManager.requestAVAsset(forVideo: asset, options: options) { (avAsset, _, info) in
|
||||
if let avAsset = avAsset {
|
||||
subscriber.putNext(avAsset)
|
||||
|
@ -154,15 +154,20 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
|
||||
var _cachedTag: Int32?
|
||||
var tag: Int32? {
|
||||
// if let tag = self._cachedTag {
|
||||
// return tag
|
||||
// } else if let asset = self.asset, let localTimestamp = asset.creationDate?.timeIntervalSince1970 {
|
||||
// let tag = Month(localTimestamp: Int32(localTimestamp)).packedValue
|
||||
// self._cachedTag = tag
|
||||
// return tag
|
||||
// } else {
|
||||
if let tag = self._cachedTag {
|
||||
return tag
|
||||
} else if let (fetchResult, index) = self.currentState {
|
||||
let asset = fetchResult.object(at: index)
|
||||
if let localTimestamp = asset.creationDate?.timeIntervalSince1970 {
|
||||
let tag = Month(localTimestamp: Int32(localTimestamp)).packedValue
|
||||
self._cachedTag = tag
|
||||
return tag
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
func updateSelectionState(animated: Bool = false) {
|
||||
@ -226,58 +231,6 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
self.backgroundColor = theme.list.mediaPlaceholderColor
|
||||
|
||||
if self.currentMediaState == nil || self.currentMediaState!.0.uniqueIdentifier != media.identifier || self.currentState!.1 != index {
|
||||
// let editingContext = interaction.editingState
|
||||
// let asset = media.asset as? TGMediaEditableItem
|
||||
//
|
||||
// let editedSignal = Signal<UIImage?, NoError> { subscriber in
|
||||
// if let signal = editingContext.thumbnailImageSignal(forIdentifier: media.identifier) {
|
||||
// let disposable = signal.start(next: { next in
|
||||
// if let image = next as? UIImage {
|
||||
// subscriber.putNext(image)
|
||||
// } else {
|
||||
// subscriber.putNext(nil)
|
||||
// }
|
||||
// }, error: { _ in
|
||||
// }, completed: nil)!
|
||||
//
|
||||
// return ActionDisposable {
|
||||
// disposable.dispose()
|
||||
// }
|
||||
// } else {
|
||||
// return EmptyDisposable
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// let originalImageSignal = Signal<UIImage?, NoError> { subscriber in
|
||||
// if let signal = asset?.thumbnailImageSignal?()
|
||||
// }
|
||||
//
|
||||
// let scale = min(2.0, UIScreenScale)
|
||||
// let targetSize = CGSize(width: 128.0 * scale, height: 128.0 * scale)
|
||||
// let originalSignal: Signal<UIImage, NoError> = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false)
|
||||
// let imageSignal: Signal<UIImage?, NoError> = editedSignal
|
||||
// |> mapToSignal { result in
|
||||
// if let result = result {
|
||||
// return .single(result)
|
||||
// } else {
|
||||
// return originalSignal
|
||||
// }
|
||||
// }
|
||||
// self.imageNode.setSignal(imageSignal)
|
||||
//
|
||||
// if case .video = media, let asset = media.asset as? TGCameraCapturedVideo {
|
||||
// self.typeIconNode.image = UIImage(bundleImageName: "Media Editor/MediaVideo")
|
||||
//
|
||||
// if self.typeIconNode.supernode == nil {
|
||||
// self.durationNode.attributedText = NSAttributedString(string: stringForDuration(Int32(asset.videoDuration)), font: Font.semibold(12.0), textColor: .white)
|
||||
//
|
||||
// self.addSubnode(self.gradientNode)
|
||||
// self.addSubnode(self.typeIconNode)
|
||||
// self.addSubnode(self.durationNode)
|
||||
// self.setNeedsLayout()
|
||||
// }
|
||||
// }
|
||||
//
|
||||
self.currentMediaState = (media.asset, index)
|
||||
self.setNeedsLayout()
|
||||
}
|
||||
@ -319,10 +272,17 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
return EmptyDisposable
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let scale = min(2.0, UIScreenScale)
|
||||
let targetSize = CGSize(width: 128.0 * scale, height: 128.0 * scale)
|
||||
let originalSignal = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false)
|
||||
|
||||
let assetImageSignal = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .fastFormat, synchronous: true)
|
||||
|> then(
|
||||
assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .highQualityFormat, synchronous: false)
|
||||
|> delay(0.03, queue: Queue.concurrentDefaultQueue())
|
||||
)
|
||||
|
||||
let originalSignal = assetImageSignal //assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, synchronous: true)
|
||||
let imageSignal: Signal<UIImage?, NoError> = editedSignal
|
||||
|> mapToSignal { result in
|
||||
if let result = result {
|
||||
|
@ -196,6 +196,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
private var placeholderNode: MediaPickerPlaceholderNode?
|
||||
private var manageNode: MediaPickerManageNode?
|
||||
private var scrollingArea: SparseItemGridScrollingArea
|
||||
private var isFastScrolling = false
|
||||
|
||||
private var selectionNode: MediaPickerSelectedListNode?
|
||||
|
||||
@ -213,12 +214,21 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
|
||||
private let hiddenMediaId = Promise<String?>(nil)
|
||||
|
||||
private var selectionGesture: MediaPickerGridSelectionGesture<TGMediaSelectableItem>?
|
||||
|
||||
private var fastScrollContentOffset = ValuePromise<CGPoint>(ignoreRepeated: true)
|
||||
private var fastScrollDisposable: Disposable?
|
||||
|
||||
private var didSetReady = false
|
||||
private let _ready = Promise<Bool>()
|
||||
var ready: Promise<Bool> {
|
||||
return self._ready
|
||||
}
|
||||
|
||||
fileprivate var isSuspended = false
|
||||
private var hasGallery = false
|
||||
private var isCameraPreviewVisible = true
|
||||
|
||||
private var validLayout: (ContainerViewLayout, CGFloat)?
|
||||
|
||||
init(controller: MediaPickerScreen) {
|
||||
@ -248,7 +258,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
self.addSubnode(self.containerNode)
|
||||
self.containerNode.addSubnode(self.backgroundNode)
|
||||
self.containerNode.addSubnode(self.gridNode)
|
||||
//self.containerNode.addSubnode(self.scrollingArea)
|
||||
self.containerNode.addSubnode(self.scrollingArea)
|
||||
|
||||
let preloadPromise = self.preloadPromise
|
||||
let updatedState: Signal<State, NoError>
|
||||
@ -357,9 +367,9 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
self.hiddenMediaDisposable?.dispose()
|
||||
self.selectionChangedDisposable?.dispose()
|
||||
self.itemsDimensionsUpdatedDisposable?.dispose()
|
||||
self.fastScrollDisposable?.dispose()
|
||||
}
|
||||
|
||||
private var selectionGesture: MediaPickerGridSelectionGesture<TGMediaSelectableItem>?
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
@ -402,16 +412,44 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
guard let strongSelf = self else {
|
||||
return nil
|
||||
}
|
||||
strongSelf.controller?.requestAttachmentMenuExpansion()
|
||||
strongSelf.isFastScrolling = true
|
||||
return strongSelf.gridNode.scrollView
|
||||
}
|
||||
self.scrollingArea.finishedScrolling = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.isFastScrolling = false
|
||||
}
|
||||
self.scrollingArea.setContentOffset = { [weak self] offset in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
// strongSelf.isFastScrolling = true
|
||||
strongSelf.gridNode.scrollView.setContentOffset(offset, animated: false)
|
||||
// strongSelf.isFastScrolling = false
|
||||
Queue.concurrentDefaultQueue().async {
|
||||
strongSelf.fastScrollContentOffset.set(offset)
|
||||
}
|
||||
}
|
||||
self.gridNode.visibleItemsUpdated = { [weak self] _ in
|
||||
self?.updateScrollingArea()
|
||||
|
||||
if let self, let cameraView = self.cameraView {
|
||||
self.isCameraPreviewVisible = self.gridNode.scrollView.bounds.intersects(cameraView.frame)
|
||||
self.updateIsCameraActive()
|
||||
}
|
||||
}
|
||||
self.updateScrollingArea()
|
||||
|
||||
let throttledContentOffsetSignal = self.fastScrollContentOffset.get()
|
||||
|> mapToThrottled { next -> Signal<CGPoint, NoError> in
|
||||
return .single(next) |> then(.complete() |> delay(0.02, queue: Queue.concurrentDefaultQueue()))
|
||||
}
|
||||
self.fastScrollDisposable = (throttledContentOffsetSignal
|
||||
|> deliverOnMainQueue).start(next: { [weak self] contentOffset in
|
||||
if let self {
|
||||
self.gridNode.scrollView.setContentOffset(contentOffset, animated: false)
|
||||
}
|
||||
})
|
||||
|
||||
if let controller = self.controller, case .assets(nil) = controller.subject {
|
||||
let enableAnimations = self.controller?.context.sharedContext.energyUsageSettings.fullTranslucency ?? true
|
||||
@ -442,6 +480,15 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
}
|
||||
}
|
||||
|
||||
func updateIsCameraActive() {
|
||||
let isCameraActive = !self.isSuspended && !self.hasGallery && self.isCameraPreviewVisible
|
||||
if isCameraActive {
|
||||
self.cameraView?.resumePreview()
|
||||
} else {
|
||||
self.cameraView?.pausePreview()
|
||||
}
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if otherGestureRecognizer.view is UIScrollView || otherGestureRecognizer is UIPanGestureRecognizer {
|
||||
return true
|
||||
@ -706,9 +753,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
self?.controller?.present(c, in: .window(.root), with: a)
|
||||
}, finishedTransitionIn: { [weak self] in
|
||||
self?.openingMedia = false
|
||||
self?.cameraView?.pausePreview()
|
||||
self?.hasGallery = true
|
||||
self?.updateIsCameraActive()
|
||||
}, willTransitionOut: { [weak self] in
|
||||
self?.cameraView?.resumePreview()
|
||||
self?.hasGallery = false
|
||||
self?.updateIsCameraActive()
|
||||
}, dismissAll: { [weak self] in
|
||||
self?.controller?.dismissAll()
|
||||
})
|
||||
@ -742,9 +791,11 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
self?.controller?.present(c, in: .window(.root), with: a, blockInteraction: true)
|
||||
}, finishedTransitionIn: { [weak self] in
|
||||
self?.openingMedia = false
|
||||
self?.cameraView?.pausePreview()
|
||||
self?.hasGallery = true
|
||||
self?.updateIsCameraActive()
|
||||
}, willTransitionOut: { [weak self] in
|
||||
self?.cameraView?.resumePreview()
|
||||
self?.hasGallery = false
|
||||
self?.updateIsCameraActive()
|
||||
}, dismissAll: { [weak self] in
|
||||
self?.controller?.dismissAll()
|
||||
})
|
||||
@ -1590,12 +1641,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
}
|
||||
self.scrollToTop?()
|
||||
|
||||
self.controllerNode.cameraView?.pausePreview()
|
||||
self.controllerNode.isSuspended = true
|
||||
self.controllerNode.updateIsCameraActive()
|
||||
}
|
||||
|
||||
public func prepareForReuse() {
|
||||
self.controllerNode.cameraView?.resumePreview()
|
||||
|
||||
self.controllerNode.isSuspended = false
|
||||
self.controllerNode.updateIsCameraActive()
|
||||
self.controllerNode.updateNavigation(delayDisappear: true, transition: .immediate)
|
||||
}
|
||||
|
||||
|
@ -711,6 +711,10 @@ NSString *suffix = @"";
|
||||
[platform isEqualToString:@"iPad11,7"])
|
||||
return @"iPad (8th gen)";
|
||||
|
||||
if ([platform isEqualToString:@"iPad12,1"] ||
|
||||
[platform isEqualToString:@"iPad12,2"])
|
||||
return @"iPad (9th gen)";
|
||||
|
||||
if ([platform isEqualToString:@"iPad13,1"] ||
|
||||
[platform isEqualToString:@"iPad13,2"])
|
||||
return @"iPad Air (4th gen)";
|
||||
|
@ -1032,7 +1032,7 @@ public final class PremiumGiftScreen: ViewControllerComponentContainer, Attachme
|
||||
completion: { duration in
|
||||
completionImpl?(duration)
|
||||
}
|
||||
), navigationBarAppearance: .transparent)
|
||||
), navigationBarAppearance: .transparent, presentationMode: .modal)
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
|
@ -1350,6 +1350,12 @@ public final class SparseItemGrid: ASDisplayNode {
|
||||
self.currentViewport?.scrollView.isScrollEnabled = self.isScrollEnabled
|
||||
}
|
||||
}
|
||||
|
||||
public func scrollWithDelta(_ delta: CGFloat) {
|
||||
if let scrollView = self.currentViewport?.scrollView {
|
||||
scrollView.setContentOffset(CGPoint(x: 0.0, y: scrollView.contentOffset.y + delta), animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
public init(theme: PresentationTheme, initialZoomLevel: ZoomLevel? = nil) {
|
||||
self.theme = theme
|
||||
|
@ -948,6 +948,7 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
||||
private var activityTimer: SwiftSignalKit.Timer?
|
||||
|
||||
public var beginScrolling: (() -> UIScrollView?)?
|
||||
public var finishedScrolling: (() -> Void)?
|
||||
public var setContentOffset: ((CGPoint) -> Void)?
|
||||
public var openCurrentDate: (() -> Void)?
|
||||
|
||||
@ -1059,6 +1060,8 @@ public final class SparseItemGridScrollingArea: ASDisplayNode {
|
||||
strongSelf.updateLineIndicator(transition: transition)
|
||||
|
||||
strongSelf.updateActivityTimer(isScrolling: false)
|
||||
|
||||
strongSelf.finishedScrolling?()
|
||||
},
|
||||
moved: { [weak self] relativeOffset in
|
||||
guard let strongSelf = self else {
|
||||
|
@ -303,7 +303,13 @@ open class TelegramBaseController: ViewController, KeyShortcutResponder {
|
||||
disposable.set((callContext.context.panelData
|
||||
|> deliverOnMainQueue).start(next: { panelData in
|
||||
callContext.keep()
|
||||
subscriber.putNext(panelData)
|
||||
var updatedPanelData = panelData
|
||||
if let panelData {
|
||||
var updatedInfo = panelData.info
|
||||
updatedInfo.subscribedToScheduled = activeCall.subscribedToScheduled
|
||||
updatedPanelData = panelData.withInfo(updatedInfo)
|
||||
}
|
||||
subscriber.putNext(updatedPanelData)
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -64,6 +64,18 @@ public final class GroupCallPanelData {
|
||||
self.activeSpeakers = activeSpeakers
|
||||
self.groupCall = groupCall
|
||||
}
|
||||
|
||||
public func withInfo(_ info: GroupCallInfo) -> GroupCallPanelData {
|
||||
return GroupCallPanelData(
|
||||
peerId: self.peerId,
|
||||
isChannel: self.isChannel,
|
||||
info: info,
|
||||
topParticipants: self.topParticipants,
|
||||
participantCount: self.participantCount,
|
||||
activeSpeakers: self.activeSpeakers,
|
||||
groupCall: self.groupCall
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private final class FakeAudioLevelGenerator {
|
||||
|
@ -116,12 +116,12 @@ public final class AccountGroupCallContextImpl: AccountGroupCallContext {
|
||||
activeSpeakers: Set(),
|
||||
groupCall: nil
|
||||
)))*/
|
||||
|
||||
|
||||
let state = engine.calls.getGroupCallParticipants(callId: call.id, accessHash: call.accessHash, offset: "", ssrcs: [], limit: 100, sortAscending: nil)
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<GroupCallParticipantsContext.State?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|> map(Optional.init)
|
||||
|> `catch` { _ -> Signal<GroupCallParticipantsContext.State?, NoError> in
|
||||
return .single(nil)
|
||||
}
|
||||
|
||||
self.disposable = (combineLatest(queue: .mainQueue(),
|
||||
state,
|
||||
@ -139,7 +139,7 @@ public final class AccountGroupCallContextImpl: AccountGroupCallContext {
|
||||
state: state,
|
||||
previousServiceState: nil
|
||||
)
|
||||
|
||||
|
||||
strongSelf.participantsContext = context
|
||||
strongSelf.panelDataPromise.set(combineLatest(queue: .mainQueue(),
|
||||
context.state,
|
||||
|
@ -104,7 +104,7 @@ func telegramMediaActionFromApiAction(_ action: Api.MessageAction) -> TelegramMe
|
||||
components.append(.isHidden(hidden == .boolTrue))
|
||||
}
|
||||
return TelegramMediaAction(action: .topicEdited(components: components))
|
||||
case let.messageActionSuggestProfilePhoto(photo):
|
||||
case let .messageActionSuggestProfilePhoto(photo):
|
||||
return TelegramMediaAction(action: .suggestedProfilePhoto(image: telegramMediaImageFromApiPhoto(photo)))
|
||||
case let .messageActionRequestedPeer(buttonId, peer):
|
||||
return TelegramMediaAction(action: .requestedPeer(buttonId: buttonId, peerId: peer.peerId))
|
||||
|
@ -239,7 +239,7 @@ final class NetworkFrameworkTcpConnectionInterface: NSObject, MTTcpConnectionInt
|
||||
|
||||
if data.count != 0 && data.count <= currentReadRequest.request.length - currentReadRequest.readyLength {
|
||||
currentReadRequest.data.withUnsafeMutableBytes { currentBuffer in
|
||||
guard let currentBytes = currentBuffer.assumingMemoryBound(to: UInt8.self).baseAddress else {
|
||||
guard let currentBytes = currentBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else {
|
||||
return
|
||||
}
|
||||
data.copyBytes(to: currentBytes.advanced(by: currentReadRequest.readyLength), count: data.count)
|
||||
|
@ -1026,7 +1026,7 @@ public final class AccountStateManager {
|
||||
}
|
||||
|
||||
let _ = (signal
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] messages in
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] messages in
|
||||
if let strongSelf = self {
|
||||
strongSelf.notificationMessagesPipe.putNext(messages)
|
||||
}
|
||||
@ -1953,6 +1953,9 @@ public func messagesForNotification(transaction: Transaction, id: MessageId, alw
|
||||
}
|
||||
|
||||
if let channel = message.peers[message.id.peerId] as? TelegramChannel {
|
||||
if !channel.flags.contains(.isForum) {
|
||||
threadData = nil
|
||||
}
|
||||
switch channel.participationStatus {
|
||||
case .kicked, .left:
|
||||
return ([], false, sound, false, threadData)
|
||||
|
@ -457,8 +457,13 @@ extension EngineChatList.Item {
|
||||
|
||||
let readCounters = readState.flatMap(EnginePeerReadCounters.init)
|
||||
|
||||
if let channel = renderedPeer.peer as? TelegramChannel, channel.flags.contains(.isForum) {
|
||||
draft = nil
|
||||
if let channel = renderedPeer.peer as? TelegramChannel {
|
||||
if channel.flags.contains(.isForum) {
|
||||
draft = nil
|
||||
} else {
|
||||
forumTopicDataValue = nil
|
||||
topForumTopicItems = []
|
||||
}
|
||||
}
|
||||
|
||||
self.init(
|
||||
|
@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
@ -95,6 +96,7 @@ private final class TitleFieldComponent: Component {
|
||||
let iconColor: Int32
|
||||
let text: String
|
||||
let placeholderText: String
|
||||
let isEditing: Bool
|
||||
let textUpdated: (String) -> Void
|
||||
let iconPressed: () -> Void
|
||||
|
||||
@ -108,6 +110,7 @@ private final class TitleFieldComponent: Component {
|
||||
iconColor: Int32,
|
||||
text: String,
|
||||
placeholderText: String,
|
||||
isEditing: Bool,
|
||||
textUpdated: @escaping (String) -> Void,
|
||||
iconPressed: @escaping () -> Void
|
||||
) {
|
||||
@ -120,6 +123,7 @@ private final class TitleFieldComponent: Component {
|
||||
self.iconColor = iconColor
|
||||
self.text = text
|
||||
self.placeholderText = placeholderText
|
||||
self.isEditing = isEditing
|
||||
self.textUpdated = textUpdated
|
||||
self.iconPressed = iconPressed
|
||||
}
|
||||
@ -152,6 +156,9 @@ private final class TitleFieldComponent: Component {
|
||||
if lhs.placeholderText != rhs.placeholderText {
|
||||
return false
|
||||
}
|
||||
if lhs.isEditing != rhs.isEditing {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -237,6 +244,7 @@ private final class TitleFieldComponent: Component {
|
||||
iconContent = .animation(content: .customEmoji(fileId: component.fileId), size: CGSize(width: 48.0, height: 48.0), placeholderColor: component.placeholderColor, themeColor: component.accentColor, loopMode: .count(2))
|
||||
self.iconButton.isUserInteractionEnabled = false
|
||||
}
|
||||
self.iconButton.isUserInteractionEnabled = !component.isEditing
|
||||
|
||||
let placeholderSize = self.placeholderView.update(
|
||||
transition: .easeInOut(duration: 0.2),
|
||||
@ -471,15 +479,26 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
|
||||
let mode: ForumCreateTopicScreen.Mode
|
||||
let titleUpdated: (String) -> Void
|
||||
let iconUpdated: (Int64?) -> Void
|
||||
let iconColorUpdated: (Int32) -> Void
|
||||
let isHiddenUpdated: (Bool) -> Void
|
||||
let openPremium: () -> Void
|
||||
|
||||
init(context: AccountContext, peerId: EnginePeer.Id, mode: ForumCreateTopicScreen.Mode, titleUpdated: @escaping (String) -> Void, iconUpdated: @escaping (Int64?) -> Void, isHiddenUpdated: @escaping (Bool) -> Void, openPremium: @escaping () -> Void) {
|
||||
init(
|
||||
context: AccountContext,
|
||||
peerId: EnginePeer.Id,
|
||||
mode: ForumCreateTopicScreen.Mode,
|
||||
titleUpdated: @escaping (String) -> Void,
|
||||
iconUpdated: @escaping (Int64?) -> Void,
|
||||
iconColorUpdated: @escaping (Int32) -> Void,
|
||||
isHiddenUpdated: @escaping (Bool) -> Void,
|
||||
openPremium: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.peerId = peerId
|
||||
self.mode = mode
|
||||
self.titleUpdated = titleUpdated
|
||||
self.iconUpdated = iconUpdated
|
||||
self.iconColorUpdated = iconColorUpdated
|
||||
self.isHiddenUpdated = isHiddenUpdated
|
||||
self.openPremium = openPremium
|
||||
}
|
||||
@ -501,6 +520,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
|
||||
private let context: AccountContext
|
||||
private let titleUpdated: (String) -> Void
|
||||
private let iconUpdated: (Int64?) -> Void
|
||||
private let iconColorUpdated: (Int32) -> Void
|
||||
private let isHiddenUpdated: (Bool) -> Void
|
||||
private let openPremium: () -> Void
|
||||
|
||||
@ -520,10 +540,11 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
|
||||
|
||||
private var hasPremium: Bool = false
|
||||
|
||||
init(context: AccountContext, mode: ForumCreateTopicScreen.Mode, titleUpdated: @escaping (String) -> Void, iconUpdated: @escaping (Int64?) -> Void, isHiddenUpdated: @escaping (Bool) -> Void, openPremium: @escaping () -> Void) {
|
||||
init(context: AccountContext, mode: ForumCreateTopicScreen.Mode, titleUpdated: @escaping (String) -> Void, iconUpdated: @escaping (Int64?) -> Void, iconColorUpdated: @escaping (Int32) -> Void, isHiddenUpdated: @escaping (Bool) -> Void, openPremium: @escaping () -> Void) {
|
||||
self.context = context
|
||||
self.titleUpdated = titleUpdated
|
||||
self.iconUpdated = iconUpdated
|
||||
self.iconColorUpdated = iconColorUpdated
|
||||
self.isHiddenUpdated = isHiddenUpdated
|
||||
self.openPremium = openPremium
|
||||
|
||||
@ -534,6 +555,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
|
||||
self.fileId = 0
|
||||
self.iconColor = ForumCreateTopicScreen.iconColors.randomElement() ?? 0x0
|
||||
self.isHidden = false
|
||||
iconColorUpdated(self.iconColor)
|
||||
case let .edit(threadId, info, isHidden):
|
||||
self.isGeneral = threadId == 1
|
||||
self.title = info.title
|
||||
@ -647,6 +669,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
|
||||
self.iconColor = colors.first ?? 0
|
||||
}
|
||||
self.updated(transition: .immediate)
|
||||
self.iconColorUpdated(self.iconColor)
|
||||
self.updateEmojiContent()
|
||||
}
|
||||
|
||||
@ -678,6 +701,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
|
||||
mode: self.mode,
|
||||
titleUpdated: self.titleUpdated,
|
||||
iconUpdated: self.iconUpdated,
|
||||
iconColorUpdated: self.iconColorUpdated,
|
||||
isHiddenUpdated: self.isHiddenUpdated,
|
||||
openPremium: self.openPremium
|
||||
)
|
||||
@ -753,6 +777,11 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: contentHeight + titleBackground.size.height / 2.0))
|
||||
)
|
||||
|
||||
var isEditing = false
|
||||
if case .edit = context.component.mode {
|
||||
isEditing = true
|
||||
}
|
||||
|
||||
let titleField = titleField.update(
|
||||
component: TitleFieldComponent(
|
||||
context: context.component.context,
|
||||
@ -764,6 +793,7 @@ private final class ForumCreateTopicScreenComponent: CombinedComponent {
|
||||
iconColor: state.iconColor,
|
||||
text: state.title,
|
||||
placeholderText: environment.strings.CreateTopic_EnterTopicTitlePlaceholder,
|
||||
isEditing: isEditing,
|
||||
textUpdated: { [weak state] text in
|
||||
state?.updateTitle(text)
|
||||
},
|
||||
@ -999,8 +1029,8 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer {
|
||||
|
||||
private var doneBarItem: UIBarButtonItem?
|
||||
|
||||
private var state: (String, Int64?, Bool?) = ("", nil, nil)
|
||||
public var completion: (String, Int64?, Bool?) -> Void = { _, _, _ in }
|
||||
private var state: (title: String, icon: Int64?, iconColor: Int32, isHidden: Bool?) = ("", nil, 0, nil)
|
||||
public var completion: (_ title: String, _ icon: Int64?, _ iconColor: Int32, _ isHidden: Bool?) -> Void = { _, _, _, _ in }
|
||||
|
||||
public var isInProgress: Bool = false {
|
||||
didSet {
|
||||
@ -1021,6 +1051,7 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer {
|
||||
|
||||
var titleUpdatedImpl: ((String) -> Void)?
|
||||
var iconUpdatedImpl: ((Int64?) -> Void)?
|
||||
var iconColorUpdatedImpl: ((Int32) -> Void)?
|
||||
var isHiddenUpdatedImpl: ((Bool) -> Void)?
|
||||
var openPremiumImpl: (() -> Void)?
|
||||
|
||||
@ -1028,6 +1059,8 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer {
|
||||
titleUpdatedImpl?(title)
|
||||
}, iconUpdated: { fileId in
|
||||
iconUpdatedImpl?(fileId)
|
||||
}, iconColorUpdated: { iconColor in
|
||||
iconColorUpdatedImpl?(iconColor)
|
||||
}, isHiddenUpdated: { isHidden in
|
||||
isHiddenUpdatedImpl?(isHidden)
|
||||
}, openPremium: {
|
||||
@ -1045,7 +1078,7 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer {
|
||||
title = presentationData.strings.CreateTopic_EditTitle
|
||||
doneTitle = presentationData.strings.Common_Done
|
||||
|
||||
self.state = (topic.title, topic.icon, threadId == 1 ? isHidden : nil)
|
||||
self.state = (topic.title, topic.icon, topic.iconColor, threadId == 1 ? isHidden : nil)
|
||||
}
|
||||
|
||||
self.title = title
|
||||
@ -1066,23 +1099,28 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer {
|
||||
}
|
||||
strongSelf.doneBarItem?.isEnabled = !title.isEmpty
|
||||
|
||||
strongSelf.state = (title, strongSelf.state.1, strongSelf.state.2)
|
||||
strongSelf.state = (title, strongSelf.state.icon, strongSelf.state.iconColor, strongSelf.state.isHidden)
|
||||
}
|
||||
|
||||
iconUpdatedImpl = { [weak self] fileId in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.state = (strongSelf.state.0, fileId, strongSelf.state.2)
|
||||
strongSelf.state = (strongSelf.state.title, fileId, strongSelf.state.iconColor, strongSelf.state.isHidden)
|
||||
}
|
||||
|
||||
iconColorUpdatedImpl = { [weak self] iconColor in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
strongSelf.state = (strongSelf.state.title, strongSelf.state.icon, iconColor, strongSelf.state.isHidden)
|
||||
}
|
||||
|
||||
isHiddenUpdatedImpl = { [weak self] isHidden in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
|
||||
strongSelf.state = (strongSelf.state.0, strongSelf.state.1, isHidden)
|
||||
strongSelf.state = (strongSelf.state.title, strongSelf.state.icon, strongSelf.state.iconColor, isHidden)
|
||||
}
|
||||
|
||||
openPremiumImpl = { [weak self] in
|
||||
@ -1113,6 +1151,6 @@ public class ForumCreateTopicScreen: ViewControllerComponentContainer {
|
||||
}
|
||||
|
||||
@objc private func createPressed() {
|
||||
self.completion(self.state.0, self.state.1, self.state.2)
|
||||
self.completion(self.state.title, self.state.icon, self.state.iconColor, self.state.isHidden)
|
||||
}
|
||||
}
|
||||
|
@ -2395,13 +2395,13 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
||||
if response.actionIdentifier == UNNotificationDefaultActionIdentifier {
|
||||
if let (peerId, threadId) = peerIdFromNotification(response.notification) {
|
||||
var messageId: MessageId? = nil
|
||||
if response.notification.request.content.categoryIdentifier == "watch" {
|
||||
if response.notification.request.content.categoryIdentifier == "c" {
|
||||
messageId = messageIdFromNotification(peerId: peerId, notification: response.notification)
|
||||
}
|
||||
self.openChatWhenReady(accountId: accountId, peerId: peerId, threadId: threadId, messageId: messageId)
|
||||
}
|
||||
completionHandler()
|
||||
} else if response.actionIdentifier == "reply", let (peerId, _) = peerIdFromNotification(response.notification), let accountId = accountId {
|
||||
} else if response.actionIdentifier == "reply", let (peerId, threadId) = peerIdFromNotification(response.notification), let accountId = accountId {
|
||||
guard let response = response as? UNTextInputNotificationResponse, !response.userText.isEmpty else {
|
||||
completionHandler()
|
||||
return
|
||||
@ -2427,7 +2427,11 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
||||
if let messageId = messageIdFromNotification(peerId: peerId, notification: response.notification) {
|
||||
let _ = TelegramEngine(account: account).messages.applyMaxReadIndexInteractively(index: MessageIndex(id: messageId, timestamp: 0)).start()
|
||||
}
|
||||
return enqueueMessages(account: account, peerId: peerId, messages: [EnqueueMessage.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])])
|
||||
var replyToMessageId: MessageId?
|
||||
if let threadId {
|
||||
replyToMessageId = MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId))
|
||||
}
|
||||
return enqueueMessages(account: account, peerId: peerId, messages: [EnqueueMessage.message(text: text, attributes: [], inlineStickers: [:], mediaReference: nil, replyToMessageId: replyToMessageId, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: [])])
|
||||
|> map { messageIds -> MessageId? in
|
||||
if messageIds.isEmpty {
|
||||
return nil
|
||||
@ -2489,123 +2493,82 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
||||
}
|
||||
|
||||
private func registerForNotifications(replyString: String, messagePlaceholderString: String, hiddenContentString: String, includeNames: Bool, authorize: Bool = true, completion: @escaping (Bool) -> Void = { _ in }) {
|
||||
if #available(iOS 10.0, *) {
|
||||
let notificationCenter = UNUserNotificationCenter.current()
|
||||
Logger.shared.log("App \(self.episodeId)", "register for notifications: get settings (authorize: \(authorize))")
|
||||
notificationCenter.getNotificationSettings(completionHandler: { settings in
|
||||
Logger.shared.log("App \(self.episodeId)", "register for notifications: received settings: \(settings.authorizationStatus)")
|
||||
|
||||
switch (settings.authorizationStatus, authorize) {
|
||||
case (.authorized, _), (.notDetermined, true):
|
||||
var authorizationOptions: UNAuthorizationOptions = [.badge, .sound, .alert, .carPlay]
|
||||
if #available(iOS 12.0, *) {
|
||||
authorizationOptions.insert(.providesAppNotificationSettings)
|
||||
}
|
||||
if #available(iOS 13.0, *) {
|
||||
authorizationOptions.insert(.announcement)
|
||||
}
|
||||
Logger.shared.log("App \(self.episodeId)", "register for notifications: request authorization")
|
||||
notificationCenter.requestAuthorization(options: authorizationOptions, completionHandler: { result, _ in
|
||||
Logger.shared.log("App \(self.episodeId)", "register for notifications: received authorization: \(result)")
|
||||
completion(result)
|
||||
if result {
|
||||
Queue.mainQueue().async {
|
||||
let reply = UNTextInputNotificationAction(identifier: "reply", title: replyString, options: [], textInputButtonTitle: replyString, textInputPlaceholder: messagePlaceholderString)
|
||||
|
||||
let unknownMessageCategory: UNNotificationCategory
|
||||
let replyMessageCategory: UNNotificationCategory
|
||||
let replyLegacyMessageCategory: UNNotificationCategory
|
||||
let replyLegacyMediaMessageCategory: UNNotificationCategory
|
||||
let replyMediaMessageCategory: UNNotificationCategory
|
||||
let legacyChannelMessageCategory: UNNotificationCategory
|
||||
let muteMessageCategory: UNNotificationCategory
|
||||
let muteMediaMessageCategory: UNNotificationCategory
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
var options: UNNotificationCategoryOptions = []
|
||||
if includeNames {
|
||||
options.insert(.hiddenPreviewsShowTitle)
|
||||
}
|
||||
|
||||
var carPlayOptions = options
|
||||
carPlayOptions.insert(.allowInCarPlay)
|
||||
if #available(iOS 13.2, *) {
|
||||
carPlayOptions.insert(.allowAnnouncement)
|
||||
}
|
||||
|
||||
unknownMessageCategory = UNNotificationCategory(identifier: "unknown", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
|
||||
replyMessageCategory = UNNotificationCategory(identifier: "withReply", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: carPlayOptions)
|
||||
replyLegacyMessageCategory = UNNotificationCategory(identifier: "r", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: carPlayOptions)
|
||||
replyLegacyMediaMessageCategory = UNNotificationCategory(identifier: "m", actions: [reply], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: carPlayOptions)
|
||||
replyMediaMessageCategory = UNNotificationCategory(identifier: "withReplyMedia", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: carPlayOptions)
|
||||
legacyChannelMessageCategory = UNNotificationCategory(identifier: "c", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
|
||||
muteMessageCategory = UNNotificationCategory(identifier: "withMute", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
|
||||
muteMediaMessageCategory = UNNotificationCategory(identifier: "withMuteMedia", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
|
||||
} else {
|
||||
let carPlayOptions: UNNotificationCategoryOptions = [.allowInCarPlay]
|
||||
|
||||
unknownMessageCategory = UNNotificationCategory(identifier: "unknown", actions: [], intentIdentifiers: [], options: [])
|
||||
replyMessageCategory = UNNotificationCategory(identifier: "withReply", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], options: carPlayOptions)
|
||||
replyLegacyMessageCategory = UNNotificationCategory(identifier: "r", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], options: carPlayOptions)
|
||||
replyLegacyMediaMessageCategory = UNNotificationCategory(identifier: "m", actions: [reply], intentIdentifiers: [], options: [])
|
||||
replyMediaMessageCategory = UNNotificationCategory(identifier: "withReplyMedia", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], options: carPlayOptions)
|
||||
legacyChannelMessageCategory = UNNotificationCategory(identifier: "c", actions: [], intentIdentifiers: [], options: [])
|
||||
muteMessageCategory = UNNotificationCategory(identifier: "withMute", actions: [], intentIdentifiers: [], options: [])
|
||||
muteMediaMessageCategory = UNNotificationCategory(identifier: "withMuteMedia", actions: [], intentIdentifiers: [], options: [])
|
||||
let notificationCenter = UNUserNotificationCenter.current()
|
||||
Logger.shared.log("App \(self.episodeId)", "register for notifications: get settings (authorize: \(authorize))")
|
||||
notificationCenter.getNotificationSettings(completionHandler: { settings in
|
||||
Logger.shared.log("App \(self.episodeId)", "register for notifications: received settings: \(settings.authorizationStatus)")
|
||||
|
||||
switch (settings.authorizationStatus, authorize) {
|
||||
case (.authorized, _), (.notDetermined, true):
|
||||
var authorizationOptions: UNAuthorizationOptions = [.badge, .sound, .alert, .carPlay]
|
||||
if #available(iOS 12.0, *) {
|
||||
authorizationOptions.insert(.providesAppNotificationSettings)
|
||||
}
|
||||
if #available(iOS 13.0, *) {
|
||||
authorizationOptions.insert(.announcement)
|
||||
}
|
||||
Logger.shared.log("App \(self.episodeId)", "register for notifications: request authorization")
|
||||
notificationCenter.requestAuthorization(options: authorizationOptions, completionHandler: { result, _ in
|
||||
Logger.shared.log("App \(self.episodeId)", "register for notifications: received authorization: \(result)")
|
||||
completion(result)
|
||||
if result {
|
||||
Queue.mainQueue().async {
|
||||
let reply = UNTextInputNotificationAction(identifier: "reply", title: replyString, options: [], textInputButtonTitle: replyString, textInputPlaceholder: messagePlaceholderString)
|
||||
|
||||
let unknownMessageCategory: UNNotificationCategory
|
||||
let repliableMessageCategory: UNNotificationCategory
|
||||
let repliableMediaMessageCategory: UNNotificationCategory
|
||||
let groupRepliableMessageCategory: UNNotificationCategory
|
||||
let groupRepliableMediaMessageCategory: UNNotificationCategory
|
||||
let channelMessageCategory: UNNotificationCategory
|
||||
|
||||
if #available(iOS 11.0, *) {
|
||||
var options: UNNotificationCategoryOptions = []
|
||||
if includeNames {
|
||||
options.insert(.hiddenPreviewsShowTitle)
|
||||
}
|
||||
|
||||
UNUserNotificationCenter.current().setNotificationCategories([unknownMessageCategory, replyMessageCategory, replyLegacyMessageCategory, replyLegacyMediaMessageCategory, replyMediaMessageCategory, legacyChannelMessageCategory, muteMessageCategory, muteMediaMessageCategory])
|
||||
var carPlayOptions = options
|
||||
carPlayOptions.insert(.allowInCarPlay)
|
||||
if #available(iOS 13.2, *) {
|
||||
carPlayOptions.insert(.allowAnnouncement)
|
||||
}
|
||||
|
||||
Logger.shared.log("App \(self.episodeId)", "register for notifications: invoke registerForRemoteNotifications")
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
unknownMessageCategory = UNNotificationCategory(identifier: "unknown", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
|
||||
repliableMessageCategory = UNNotificationCategory(identifier: "r", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: carPlayOptions)
|
||||
repliableMediaMessageCategory = UNNotificationCategory(identifier: "m", actions: [reply], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: carPlayOptions)
|
||||
groupRepliableMessageCategory = UNNotificationCategory(identifier: "gr", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
|
||||
groupRepliableMediaMessageCategory = UNNotificationCategory(identifier: "gm", actions: [reply], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
|
||||
channelMessageCategory = UNNotificationCategory(identifier: "c", actions: [], intentIdentifiers: [], hiddenPreviewsBodyPlaceholder: hiddenContentString, options: options)
|
||||
} else {
|
||||
let carPlayOptions: UNNotificationCategoryOptions = [.allowInCarPlay]
|
||||
|
||||
unknownMessageCategory = UNNotificationCategory(identifier: "unknown", actions: [], intentIdentifiers: [], options: [])
|
||||
repliableMessageCategory = UNNotificationCategory(identifier: "r", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], options: carPlayOptions)
|
||||
repliableMediaMessageCategory = UNNotificationCategory(identifier: "m", actions: [reply], intentIdentifiers: [], options: [])
|
||||
groupRepliableMessageCategory = UNNotificationCategory(identifier: "gr", actions: [reply], intentIdentifiers: [INSearchForMessagesIntentIdentifier], options: [])
|
||||
groupRepliableMediaMessageCategory = UNNotificationCategory(identifier: "gm", actions: [reply], intentIdentifiers: [], options: [])
|
||||
channelMessageCategory = UNNotificationCategory(identifier: "c", actions: [], intentIdentifiers: [], options: [])
|
||||
}
|
||||
|
||||
UNUserNotificationCenter.current().setNotificationCategories([
|
||||
unknownMessageCategory,
|
||||
repliableMessageCategory,
|
||||
repliableMediaMessageCategory,
|
||||
channelMessageCategory,
|
||||
groupRepliableMessageCategory,
|
||||
groupRepliableMediaMessageCategory
|
||||
])
|
||||
|
||||
Logger.shared.log("App \(self.episodeId)", "register for notifications: invoke registerForRemoteNotifications")
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
})
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
} else {
|
||||
let reply = UIMutableUserNotificationAction()
|
||||
reply.identifier = "reply"
|
||||
reply.title = replyString
|
||||
reply.isDestructive = false
|
||||
reply.isAuthenticationRequired = false
|
||||
reply.behavior = .textInput
|
||||
reply.activationMode = .background
|
||||
|
||||
let unknownMessageCategory = UIMutableUserNotificationCategory()
|
||||
unknownMessageCategory.identifier = "unknown"
|
||||
|
||||
let replyMessageCategory = UIMutableUserNotificationCategory()
|
||||
replyMessageCategory.identifier = "withReply"
|
||||
replyMessageCategory.setActions([reply], for: .default)
|
||||
|
||||
let replyLegacyMessageCategory = UIMutableUserNotificationCategory()
|
||||
replyLegacyMessageCategory.identifier = "r"
|
||||
replyLegacyMessageCategory.setActions([reply], for: .default)
|
||||
|
||||
let replyLegacyMediaMessageCategory = UIMutableUserNotificationCategory()
|
||||
replyLegacyMediaMessageCategory.identifier = "m"
|
||||
replyLegacyMediaMessageCategory.setActions([reply], for: .default)
|
||||
|
||||
let replyMediaMessageCategory = UIMutableUserNotificationCategory()
|
||||
replyMediaMessageCategory.identifier = "withReplyMedia"
|
||||
replyMediaMessageCategory.setActions([reply], for: .default)
|
||||
|
||||
let legacyChannelMessageCategory = UIMutableUserNotificationCategory()
|
||||
legacyChannelMessageCategory.identifier = "c"
|
||||
|
||||
let muteMessageCategory = UIMutableUserNotificationCategory()
|
||||
muteMessageCategory.identifier = "withMute"
|
||||
|
||||
let muteMediaMessageCategory = UIMutableUserNotificationCategory()
|
||||
muteMediaMessageCategory.identifier = "withMuteMedia"
|
||||
|
||||
let settings = UIUserNotificationSettings(types: [.badge, .sound, .alert], categories: [])
|
||||
UIApplication.shared.registerUserNotificationSettings(settings)
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
}
|
||||
})
|
||||
default:
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@available(iOS 10.0, *)
|
||||
|
@ -101,8 +101,6 @@ final class AuthorizedApplicationContext {
|
||||
let rootController: TelegramRootController
|
||||
let notificationController: NotificationContainerController
|
||||
|
||||
private var scheduledOpenNotificationSettings: Bool = false
|
||||
private var scheduledOpenChatWithPeerId: (PeerId, MessageId?, Bool)?
|
||||
private let scheduledCallPeerDisposable = MetaDisposable()
|
||||
private var scheduledOpenExternalUrl: URL?
|
||||
|
||||
@ -395,7 +393,7 @@ final class AuthorizedApplicationContext {
|
||||
}
|
||||
|
||||
if inAppNotificationSettings.displayPreviews {
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
strongSelf.notificationController.enqueue(ChatMessageNotificationItem(context: strongSelf.context, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat, nameDisplayOrder: presentationData.nameDisplayOrder, messages: messages, threadData: threadData, tapAction: {
|
||||
if let strongSelf = self {
|
||||
var foundOverlay = false
|
||||
@ -833,11 +831,7 @@ final class AuthorizedApplicationContext {
|
||||
}
|
||||
|
||||
func openNotificationSettings() {
|
||||
if self.rootController.rootTabController != nil {
|
||||
self.rootController.pushViewController(notificationsAndSoundsController(context: self.context, exceptionsList: nil))
|
||||
} else {
|
||||
self.scheduledOpenNotificationSettings = true
|
||||
}
|
||||
self.rootController.pushViewController(notificationsAndSoundsController(context: self.context, exceptionsList: nil))
|
||||
}
|
||||
|
||||
func startCall(peerId: PeerId, isVideo: Bool) {
|
||||
@ -864,27 +858,40 @@ final class AuthorizedApplicationContext {
|
||||
}
|
||||
|
||||
if visiblePeerId != peerId || messageId != nil {
|
||||
if self.rootController.rootTabController != nil {
|
||||
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId))
|
||||
|> deliverOnMainQueue).start(next: { peer in
|
||||
guard let peer = peer else {
|
||||
return
|
||||
}
|
||||
|
||||
let chatLocation: NavigateToChatControllerParams.Location
|
||||
if let threadId = threadId {
|
||||
chatLocation = .replyThread(ChatReplyThreadMessage(
|
||||
messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false
|
||||
))
|
||||
let isOutgoingMessage: Signal<Bool, NoError>
|
||||
if let messageId {
|
||||
let accountPeerId = self.context.account.peerId
|
||||
isOutgoingMessage = self.context.engine.data.get(TelegramEngine.EngineData.Item.Messages.Message(id: messageId))
|
||||
|> map { message -> Bool in
|
||||
if let message {
|
||||
return !message._asMessage().effectivelyIncoming(accountPeerId)
|
||||
} else {
|
||||
chatLocation = .peer(peer)
|
||||
return false
|
||||
}
|
||||
|
||||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: messageId.flatMap { .message(id: .id($0), highlight: true, timecode: nil) }, activateInput: activateInput ? .text : nil))
|
||||
})
|
||||
}
|
||||
} else {
|
||||
self.scheduledOpenChatWithPeerId = (peerId, messageId, activateInput)
|
||||
isOutgoingMessage = .single(false)
|
||||
}
|
||||
let _ = combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: peerId)),
|
||||
isOutgoingMessage
|
||||
).start(next: { peer, isOutgoingMessage in
|
||||
guard let peer = peer else {
|
||||
return
|
||||
}
|
||||
|
||||
let chatLocation: NavigateToChatControllerParams.Location
|
||||
if let threadId = threadId {
|
||||
chatLocation = .replyThread(ChatReplyThreadMessage(
|
||||
messageId: MessageId(peerId: peerId, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false
|
||||
))
|
||||
} else {
|
||||
chatLocation = .peer(peer)
|
||||
}
|
||||
|
||||
self.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: self.rootController, context: self.context, chatLocation: chatLocation, subject: isOutgoingMessage ? messageId.flatMap { .message(id: .id($0), highlight: true, timecode: nil) } : nil, activateInput: activateInput ? .text : nil))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3910,7 +3910,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|
||||
if let mediaReference = mediaReference, let peer = message.peers[message.id.peerId] {
|
||||
legacyMediaEditor(context: strongSelf.context, peer: peer, threadTitle: strongSelf.threadInfo?.title, media: mediaReference, initialCaption: NSAttributedString(string: message.text), snapshots: [], transitionCompletion: nil, getCaptionPanelView: { [weak self] in
|
||||
let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText
|
||||
legacyMediaEditor(context: strongSelf.context, peer: peer, threadTitle: strongSelf.threadInfo?.title, media: mediaReference, initialCaption: inputText, snapshots: [], transitionCompletion: nil, getCaptionPanelView: { [weak self] in
|
||||
return self?.getCaptionPanelView()
|
||||
}, sendMessagesWithSignals: { [weak self] signals, _, _ in
|
||||
if let strongSelf = self {
|
||||
@ -8139,16 +8140,15 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
if let strongSelf = self, let peerId = strongSelf.chatLocation.peerId {
|
||||
let presentationData = strongSelf.presentationData
|
||||
|
||||
let forwardOptions: Signal<ChatControllerSubject.ForwardOptions, NoError>
|
||||
if peerId.namespace == Namespaces.Peer.SecretChat {
|
||||
forwardOptions = .single(ChatControllerSubject.ForwardOptions(hideNames: true, hideCaptions: false))
|
||||
} else {
|
||||
forwardOptions = strongSelf.presentationInterfaceStatePromise.get()
|
||||
|> map { state -> ChatControllerSubject.ForwardOptions in
|
||||
return ChatControllerSubject.ForwardOptions(hideNames: state.interfaceState.forwardOptionsState?.hideNames ?? false, hideCaptions: state.interfaceState.forwardOptionsState?.hideCaptions ?? false)
|
||||
let forwardOptions = strongSelf.presentationInterfaceStatePromise.get()
|
||||
|> map { state -> ChatControllerSubject.ForwardOptions in
|
||||
var hideNames = state.interfaceState.forwardOptionsState?.hideNames ?? false
|
||||
if peerId.namespace == Namespaces.Peer.SecretChat {
|
||||
hideNames = true
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
return ChatControllerSubject.ForwardOptions(hideNames: hideNames, hideCaptions: state.interfaceState.forwardOptionsState?.hideCaptions ?? false)
|
||||
}
|
||||
|> distinctUntilChanged
|
||||
|
||||
let chatController = strongSelf.context.sharedContext.makeChatController(context: strongSelf.context, chatLocation: .peer(id: peerId), subject: .forwardedMessages(peerIds: [peerId], ids: strongSelf.presentationInterfaceState.interfaceState.forwardMessageIds ?? [], options: forwardOptions), botStart: nil, mode: .standard(previewing: true))
|
||||
chatController.canReadHistory.set(false)
|
||||
@ -8208,88 +8208,90 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
}
|
||||
|
||||
let canHideNames = hasNotOwnMessages && hasOther
|
||||
|
||||
var canHideNames = hasNotOwnMessages && hasOther
|
||||
if case let .peer(peerId) = strongSelf.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat {
|
||||
canHideNames = false
|
||||
}
|
||||
let hideNames = forwardOptions.hideNames
|
||||
let hideCaptions = forwardOptions.hideCaptions
|
||||
|
||||
if case let .peer(peerId) = strongSelf.chatLocation, peerId.namespace == Namespaces.Peer.SecretChat {
|
||||
if canHideNames {
|
||||
items.append(.action(ContextMenuActionItem(text: uniquePeerIds.count == 1 ? presentationData.strings.Conversation_ForwardOptions_ShowSendersName : presentationData.strings.Conversation_ForwardOptions_ShowSendersNames, icon: { theme in
|
||||
if hideNames {
|
||||
return nil
|
||||
} else {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
|
||||
}
|
||||
}, action: { [weak self] _, f in
|
||||
self?.interfaceInteraction?.updateForwardOptionsState({ current in
|
||||
var updated = current
|
||||
updated.hideNames = false
|
||||
updated.hideCaptions = false
|
||||
updated.unhideNamesOnCaptionChange = false
|
||||
return updated
|
||||
})
|
||||
})))
|
||||
|
||||
} else {
|
||||
if canHideNames {
|
||||
items.append(.action(ContextMenuActionItem(text: uniquePeerIds.count == 1 ? presentationData.strings.Conversation_ForwardOptions_ShowSendersName : presentationData.strings.Conversation_ForwardOptions_ShowSendersNames, icon: { theme in
|
||||
if hideNames {
|
||||
return nil
|
||||
} else {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
|
||||
}
|
||||
}, action: { [weak self] _, f in
|
||||
self?.interfaceInteraction?.updateForwardOptionsState({ current in
|
||||
var updated = current
|
||||
updated.hideNames = false
|
||||
updated.hideCaptions = false
|
||||
updated.unhideNamesOnCaptionChange = false
|
||||
return updated
|
||||
})
|
||||
})))
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: uniquePeerIds.count == 1 ? presentationData.strings.Conversation_ForwardOptions_HideSendersName : presentationData.strings.Conversation_ForwardOptions_HideSendersNames, icon: { theme in
|
||||
if hideNames {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}, action: { _, f in
|
||||
self?.interfaceInteraction?.updateForwardOptionsState({ current in
|
||||
var updated = current
|
||||
updated.hideNames = true
|
||||
updated.unhideNamesOnCaptionChange = false
|
||||
return updated
|
||||
})
|
||||
})))
|
||||
|
||||
items.append(.separator)
|
||||
}
|
||||
items.append(.action(ContextMenuActionItem(text: uniquePeerIds.count == 1 ? presentationData.strings.Conversation_ForwardOptions_HideSendersName : presentationData.strings.Conversation_ForwardOptions_HideSendersNames, icon: { theme in
|
||||
if hideNames {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}, action: { _, f in
|
||||
self?.interfaceInteraction?.updateForwardOptionsState({ current in
|
||||
var updated = current
|
||||
updated.hideNames = true
|
||||
updated.unhideNamesOnCaptionChange = false
|
||||
return updated
|
||||
})
|
||||
})))
|
||||
|
||||
if hasCaptions {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ForwardOptions_ShowCaption, icon: { theme in
|
||||
if hideCaptions {
|
||||
return nil
|
||||
} else {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
|
||||
}
|
||||
}, action: { [weak self] _, f in
|
||||
self?.interfaceInteraction?.updateForwardOptionsState({ current in
|
||||
var updated = current
|
||||
updated.hideCaptions = false
|
||||
items.append(.separator)
|
||||
}
|
||||
|
||||
if hasCaptions {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ForwardOptions_ShowCaption, icon: { theme in
|
||||
if hideCaptions {
|
||||
return nil
|
||||
} else {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
|
||||
}
|
||||
}, action: { [weak self] _, f in
|
||||
self?.interfaceInteraction?.updateForwardOptionsState({ current in
|
||||
var updated = current
|
||||
updated.hideCaptions = false
|
||||
if canHideNames {
|
||||
if updated.unhideNamesOnCaptionChange {
|
||||
updated.unhideNamesOnCaptionChange = false
|
||||
updated.hideNames = false
|
||||
}
|
||||
return updated
|
||||
})
|
||||
})))
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ForwardOptions_HideCaption, icon: { theme in
|
||||
if hideCaptions {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}, action: { _, f in
|
||||
self?.interfaceInteraction?.updateForwardOptionsState({ current in
|
||||
var updated = current
|
||||
updated.hideCaptions = true
|
||||
return updated
|
||||
})
|
||||
})))
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ForwardOptions_HideCaption, icon: { theme in
|
||||
if hideCaptions {
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}, action: { _, f in
|
||||
self?.interfaceInteraction?.updateForwardOptionsState({ current in
|
||||
var updated = current
|
||||
updated.hideCaptions = true
|
||||
if canHideNames {
|
||||
if !updated.hideNames {
|
||||
updated.hideNames = true
|
||||
updated.unhideNamesOnCaptionChange = true
|
||||
}
|
||||
return updated
|
||||
})
|
||||
})))
|
||||
|
||||
items.append(.separator)
|
||||
}
|
||||
}
|
||||
return updated
|
||||
})
|
||||
})))
|
||||
|
||||
items.append(.separator)
|
||||
}
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_ForwardOptions_ChangeRecipient, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Forward"), color: theme.contextMenu.primaryColor) }, action: { c, f in
|
||||
@ -9775,18 +9777,27 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}, openLinkEditing: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
var selectionRange: Range<Int>?
|
||||
var text: String?
|
||||
var text: NSAttributedString?
|
||||
var inputMode: ChatInputMode?
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in
|
||||
selectionRange = state.interfaceState.effectiveInputState.selectionRange
|
||||
if let selectionRange = selectionRange {
|
||||
text = state.interfaceState.effectiveInputState.inputText.attributedSubstring(from: NSRange(location: selectionRange.startIndex, length: selectionRange.count)).string
|
||||
text = state.interfaceState.effectiveInputState.inputText.attributedSubstring(from: NSRange(location: selectionRange.startIndex, length: selectionRange.count))
|
||||
}
|
||||
inputMode = state.inputMode
|
||||
return state
|
||||
})
|
||||
|
||||
let controller = chatTextLinkEditController(sharedContext: strongSelf.context.sharedContext, updatedPresentationData: strongSelf.updatedPresentationData, account: strongSelf.context.account, text: text ?? "", link: nil, apply: { [weak self] link in
|
||||
var link: String?
|
||||
if let text {
|
||||
text.enumerateAttributes(in: NSMakeRange(0, text.length)) { attributes, _, _ in
|
||||
if let linkAttribute = attributes[ChatTextInputAttributes.textUrl] as? ChatTextInputTextUrlAttribute {
|
||||
link = linkAttribute.url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let controller = chatTextLinkEditController(sharedContext: strongSelf.context.sharedContext, updatedPresentationData: strongSelf.updatedPresentationData, account: strongSelf.context.account, text: text?.string ?? "", link: link, apply: { [weak self] link in
|
||||
if let strongSelf = self, let inputMode = inputMode, let selectionRange = selectionRange {
|
||||
if let link = link {
|
||||
strongSelf.interfaceInteraction?.updateTextInputStateAndMode { current, inputMode in
|
||||
@ -11370,7 +11381,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
|
||||
var layout = layout
|
||||
if case .compact = layout.metrics.widthClass, let _ = self.attachmentController {
|
||||
if case .compact = layout.metrics.widthClass, let attachmentController = self.attachmentController, attachmentController.window != nil {
|
||||
layout = layout.withUpdatedInputHeight(nil)
|
||||
}
|
||||
|
||||
@ -12505,18 +12516,27 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}, openLinkEditing: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
var selectionRange: Range<Int>?
|
||||
var text: String?
|
||||
var text: NSAttributedString?
|
||||
var inputMode: ChatInputMode?
|
||||
updateChatPresentationInterfaceStateImpl?({ state in
|
||||
selectionRange = state.interfaceState.effectiveInputState.selectionRange
|
||||
if let selectionRange = selectionRange {
|
||||
text = state.interfaceState.effectiveInputState.inputText.attributedSubstring(from: NSRange(location: selectionRange.startIndex, length: selectionRange.count)).string
|
||||
text = state.interfaceState.effectiveInputState.inputText.attributedSubstring(from: NSRange(location: selectionRange.startIndex, length: selectionRange.count))
|
||||
}
|
||||
inputMode = state.inputMode
|
||||
return state
|
||||
})
|
||||
|
||||
let controller = chatTextLinkEditController(sharedContext: strongSelf.context.sharedContext, updatedPresentationData: (presentationData, .never()), account: strongSelf.context.account, text: text ?? "", link: nil, apply: { link in
|
||||
var link: String?
|
||||
if let text {
|
||||
text.enumerateAttributes(in: NSMakeRange(0, text.length)) { attributes, _, _ in
|
||||
if let linkAttribute = attributes[ChatTextInputAttributes.textUrl] as? ChatTextInputTextUrlAttribute {
|
||||
link = linkAttribute.url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let controller = chatTextLinkEditController(sharedContext: strongSelf.context.sharedContext, updatedPresentationData: (presentationData, .never()), account: strongSelf.context.account, text: text?.string ?? "", link: link, apply: { link in
|
||||
if let inputMode = inputMode, let selectionRange = selectionRange {
|
||||
if let link = link {
|
||||
updateChatPresentationInterfaceStateImpl?({
|
||||
@ -16011,7 +16031,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
self.dismiss()
|
||||
|
||||
let navigateToLocation: NavigateToChatControllerParams.Location
|
||||
if let message = messages.first, let threadId = message.threadId, threadId != 1 || (message.peers[message.id.peerId] as? TelegramChannel)?.flags.contains(.isForum) == true {
|
||||
if let message = messages.first, let threadId = message.threadId, let channel = message.peers[message.id.peerId] as? TelegramChannel, channel.flags.contains(.isForum) {
|
||||
navigateToLocation = .replyThread(ChatReplyThreadMessage(messageId: MessageId(peerId: peer.id, namespace: Namespaces.Message.Cloud, id: Int32(clamping: threadId)), channelMessageId: nil, isChannelPost: false, isForumPost: true, maxMessage: nil, maxReadIncomingMessageId: nil, maxReadOutgoingMessageId: nil, unreadCount: 0, initialFilledHoles: IndexSet(), initialAnchor: .automatic, isNotAvailable: false))
|
||||
} else {
|
||||
navigateToLocation = .peer(peer)
|
||||
|
@ -70,7 +70,6 @@ class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self.maskForeground.masksToBounds = true
|
||||
self.maskLayer.addSublayer(self.maskForeground)
|
||||
|
||||
self.addSubnode(self.interactiveFileNode)
|
||||
self.addSubnode(self.interactiveVideoNode)
|
||||
|
||||
self.interactiveVideoNode.requestUpdateLayout = { [weak self] _ in
|
||||
@ -285,14 +284,9 @@ class ChatMessageInstantVideoBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
|
||||
return (finalSize, { [weak self] animation, synchronousLoads, applyInfo in
|
||||
if let strongSelf = self {
|
||||
let firstTime = strongSelf.item == nil
|
||||
strongSelf.item = item
|
||||
strongSelf.isExpanded = isExpanded
|
||||
|
||||
if firstTime {
|
||||
strongSelf.interactiveFileNode.isHidden = true
|
||||
}
|
||||
|
||||
strongSelf.bubbleBackgroundNode?.layer.mask = strongSelf.maskLayer
|
||||
if let bubbleBackdropNode = strongSelf.bubbleBackdropNode, bubbleBackdropNode.hasImage && strongSelf.backdropMaskForeground.superlayer == nil {
|
||||
strongSelf.bubbleBackdropNode?.overrideMask = true
|
||||
|
@ -1601,7 +1601,9 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
let duration: Double = 0.2
|
||||
|
||||
node.alpha = 1.0
|
||||
node.isHidden = false
|
||||
if node.supernode == nil {
|
||||
self.supernode?.insertSubnode(node, belowSubnode: self)
|
||||
}
|
||||
|
||||
self.alpha = 0.0
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
||||
@ -1715,7 +1717,7 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
|
||||
node.alpha = 0.0
|
||||
node.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration, completion: { _ in
|
||||
node.isHidden = true
|
||||
node.removeFromSupernode()
|
||||
})
|
||||
node.waveformView?.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration)
|
||||
|
||||
|
@ -1687,8 +1687,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
state = .none
|
||||
badgeContent = nil
|
||||
} else if wideLayout {
|
||||
if let size = file.size {
|
||||
let sizeString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, formatting: formatting)) / \(dataSizeString(size, forceDecimal: true, formatting: formatting))"
|
||||
if let size = file.size, size > 0 && size != .max {
|
||||
let sizeString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, formatting: formatting)) / \(dataSizeString(size, forceDecimal: true, formatting: formatting))"
|
||||
if let duration = file.duration, !message.flags.contains(.Unsent) {
|
||||
let durationString = file.isAnimated ? gifTitle : stringForDuration(playerDuration > 0 ? playerDuration : duration, position: playerPosition)
|
||||
if isMediaStreamable(message: message, media: file) {
|
||||
@ -1721,8 +1721,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
state = automaticPlayback ? .none : state
|
||||
}
|
||||
} else {
|
||||
if isMediaStreamable(message: message, media: file), let size = file.size {
|
||||
let sizeString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, formatting: formatting)) / \(dataSizeString(size, forceDecimal: true, formatting: formatting))"
|
||||
if isMediaStreamable(message: message, media: file), let fileSize = file.size, fileSize > 0 && fileSize != .max {
|
||||
let sizeString = "\(dataSizeString(Int64(Float(fileSize) * progress), forceDecimal: true, formatting: formatting)) / \(dataSizeString(fileSize, forceDecimal: true, formatting: formatting))"
|
||||
|
||||
if message.flags.contains(.Unsent), let duration = file.duration {
|
||||
let durationString = stringForDuration(playerDuration > 0 ? playerDuration : duration, position: playerPosition)
|
||||
@ -1749,8 +1749,8 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
if let duration = file.duration, !file.isAnimated {
|
||||
let durationString = stringForDuration(playerDuration > 0 ? playerDuration : duration, position: playerPosition)
|
||||
|
||||
if automaticPlayback, let size = file.size {
|
||||
let sizeString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true, formatting: formatting)) / \(dataSizeString(size, forceDecimal: true, formatting: formatting))"
|
||||
if automaticPlayback, let fileSize = file.size, fileSize > 0 && fileSize != .max {
|
||||
let sizeString = "\(dataSizeString(Int64(Float(fileSize) * progress), forceDecimal: true, formatting: formatting)) / \(dataSizeString(fileSize, forceDecimal: true, formatting: formatting))"
|
||||
mediaDownloadState = .fetching(progress: progress)
|
||||
badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: durationString, size: active ? sizeString : nil, muted: muted, active: active)
|
||||
} else {
|
||||
@ -1800,9 +1800,9 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode, GalleryItemTransitio
|
||||
do {
|
||||
let durationString = file.isAnimated ? gifTitle : stringForDuration(playerDuration > 0 ? playerDuration : (file.duration ?? 0), position: playerPosition)
|
||||
if wideLayout {
|
||||
if isMediaStreamable(message: message, media: file) {
|
||||
if isMediaStreamable(message: message, media: file), let fileSize = file.size, fileSize > 0 && fileSize != .max {
|
||||
state = automaticPlayback ? .none : .play(messageTheme.mediaOverlayControlColors.foregroundColor)
|
||||
badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: durationString, size: dataSizeString(file.size ?? 0, formatting: formatting), muted: muted, active: true)
|
||||
badgeContent = .mediaDownload(backgroundColor: messageTheme.mediaDateAndStatusFillColor, foregroundColor: messageTheme.mediaDateAndStatusTextColor, duration: durationString, size: dataSizeString(fileSize, formatting: formatting), muted: muted, active: true)
|
||||
mediaDownloadState = .remote
|
||||
} else {
|
||||
state = automaticPlayback ? .none : state
|
||||
|
@ -263,19 +263,16 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
|
||||
} else if item.messages[0].id.peerId.namespace == Namespaces.Peer.CloudGroup {
|
||||
isGroup = true
|
||||
}
|
||||
title = EnginePeer(peer).displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder)
|
||||
if isChannel {
|
||||
switch kind {
|
||||
case .image:
|
||||
title = EnginePeer(peer).displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder)
|
||||
messageText = presentationData.strings.PUSH_CHANNEL_MESSAGE_PHOTOS_TEXT(Int32(item.messages.count))
|
||||
case .video:
|
||||
title = EnginePeer(peer).displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder)
|
||||
messageText = presentationData.strings.PUSH_CHANNEL_MESSAGE_VIDEOS_TEXT(Int32(item.messages.count))
|
||||
case .file:
|
||||
title = EnginePeer(peer).displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder)
|
||||
messageText = presentationData.strings.PUSH_CHANNEL_MESSAGE_DOCS_TEXT(Int32(item.messages.count))
|
||||
default:
|
||||
title = EnginePeer(peer).displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder)
|
||||
messageText = presentationData.strings.PUSH_CHANNEL_MESSAGES_TEXT(Int32(item.messages.count))
|
||||
}
|
||||
} else if isGroup, var author = item.messages[0].author {
|
||||
@ -284,31 +281,23 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
|
||||
}
|
||||
switch kind {
|
||||
case .image:
|
||||
title = EnginePeer(peer).displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder)
|
||||
messageText = presentationData.strings.PUSH_CHAT_MESSAGE_PHOTOS_TEXT(Int32(item.messages.count)).replacingOccurrences(of: "{author}", with: EnginePeer(author).compactDisplayTitle)
|
||||
case .video:
|
||||
title = EnginePeer(peer).displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder)
|
||||
messageText = presentationData.strings.PUSH_CHAT_MESSAGE_VIDEOS_TEXT(Int32(item.messages.count)).replacingOccurrences(of: "{author}", with: EnginePeer(author).compactDisplayTitle)
|
||||
case .file:
|
||||
title = EnginePeer(peer).displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder)
|
||||
messageText = presentationData.strings.PUSH_CHAT_MESSAGE_DOCS_TEXT(Int32(item.messages.count)).replacingOccurrences(of: "{author}", with: EnginePeer(author).compactDisplayTitle)
|
||||
default:
|
||||
title = EnginePeer(peer).displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder)
|
||||
messageText = presentationData.strings.PUSH_CHAT_MESSAGES_TEXT(Int32(item.messages.count)).replacingOccurrences(of: "{author}", with: EnginePeer(author).compactDisplayTitle)
|
||||
}
|
||||
} else {
|
||||
switch kind {
|
||||
case .image:
|
||||
title = EnginePeer(peer).displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder)
|
||||
messageText = presentationData.strings.PUSH_MESSAGE_PHOTOS_TEXT(Int32(item.messages.count))
|
||||
case .video:
|
||||
title = EnginePeer(peer).displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder)
|
||||
messageText = presentationData.strings.PUSH_MESSAGE_VIDEOS_TEXT(Int32(item.messages.count))
|
||||
case .file:
|
||||
title = EnginePeer(peer).displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder)
|
||||
messageText = presentationData.strings.PUSH_MESSAGE_FILES_TEXT(Int32(item.messages.count))
|
||||
default:
|
||||
title = EnginePeer(peer).displayTitle(strings: item.strings, displayOrder: item.nameDisplayOrder)
|
||||
messageText = presentationData.strings.PUSH_MESSAGES_TEXT(Int32(item.messages.count))
|
||||
}
|
||||
}
|
||||
@ -325,6 +314,10 @@ final class ChatMessageNotificationItemNode: NotificationItemNode {
|
||||
title = "📅 \(currentTitle)"
|
||||
}
|
||||
|
||||
if let attribute = item.messages.first?.attributes.first(where: { $0 is NotificationInfoMessageAttribute }) as? NotificationInfoMessageAttribute, attribute.flags.contains(.muted), let currentTitle = title {
|
||||
title = "\(currentTitle) 🔕"
|
||||
}
|
||||
|
||||
let textFont = compact ? Font.regular(15.0) : Font.regular(16.0)
|
||||
let textColor = presentationData.theme.inAppNotification.primaryTextColor
|
||||
var attributedMessageText: NSAttributedString
|
||||
|
@ -2447,7 +2447,152 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
||||
}
|
||||
|
||||
private var gridSelectionGesture: MediaPickerGridSelectionGesture<EngineMessage.Id>?
|
||||
private var listSelectionGesture: MediaPickerGridSelectionGesture<EngineMessage.Id>?
|
||||
private var listSelectionGesture: MediaListSelectionRecognizer?
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
let selectionRecognizer = MediaListSelectionRecognizer(target: self, action: #selector(self.selectionPanGesture(_:)))
|
||||
selectionRecognizer.shouldBegin = {
|
||||
return true
|
||||
}
|
||||
self.view.addGestureRecognizer(selectionRecognizer)
|
||||
}
|
||||
|
||||
private var selectionPanState: (selecting: Bool, initialMessageId: EngineMessage.Id, toggledMessageIds: [[EngineMessage.Id]])?
|
||||
private var selectionScrollActivationTimer: SwiftSignalKit.Timer?
|
||||
private var selectionScrollDisplayLink: ConstantDisplayLinkAnimator?
|
||||
private var selectionScrollDelta: CGFloat?
|
||||
private var selectionLastLocation: CGPoint?
|
||||
|
||||
private func messageAtPoint(_ location: CGPoint) -> EngineMessage? {
|
||||
if let itemView = self.itemGrid.item(at: location)?.view as? ItemView, let message = itemView.item?.message {
|
||||
return EngineMessage(message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@objc private func selectionPanGesture(_ recognizer: UIGestureRecognizer) -> Void {
|
||||
let location = recognizer.location(in: self.view)
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
if let message = self.messageAtPoint(location) {
|
||||
let selecting = !(self.chatControllerInteraction.selectionState?.selectedIds.contains(message.id) ?? false)
|
||||
self.selectionPanState = (selecting, message.id, [])
|
||||
self.chatControllerInteraction.toggleMessagesSelection([message.id], selecting)
|
||||
}
|
||||
case .changed:
|
||||
self.handlePanSelection(location: location)
|
||||
self.selectionLastLocation = location
|
||||
case .ended, .failed, .cancelled:
|
||||
self.selectionPanState = nil
|
||||
self.selectionScrollDisplayLink = nil
|
||||
self.selectionScrollActivationTimer?.invalidate()
|
||||
self.selectionScrollActivationTimer = nil
|
||||
self.selectionScrollDelta = nil
|
||||
self.selectionLastLocation = nil
|
||||
self.selectionScrollSkipUpdate = false
|
||||
case .possible:
|
||||
break
|
||||
@unknown default:
|
||||
fatalError()
|
||||
}
|
||||
}
|
||||
|
||||
private func handlePanSelection(location: CGPoint) {
|
||||
var location = location
|
||||
if location.y < 0.0 {
|
||||
location.y = 5.0
|
||||
} else if location.y > self.frame.height {
|
||||
location.y = self.frame.height - 5.0
|
||||
}
|
||||
|
||||
var hasState = false
|
||||
if let state = self.selectionPanState {
|
||||
hasState = true
|
||||
if let message = self.messageAtPoint(location) {
|
||||
if message.id == state.initialMessageId {
|
||||
if !state.toggledMessageIds.isEmpty {
|
||||
self.chatControllerInteraction.toggleMessagesSelection(state.toggledMessageIds.flatMap { $0.compactMap({ $0 }) }, !state.selecting)
|
||||
self.selectionPanState = (state.selecting, state.initialMessageId, [])
|
||||
}
|
||||
} else if state.toggledMessageIds.last?.first != message.id {
|
||||
var updatedToggledMessageIds: [[EngineMessage.Id]] = []
|
||||
var previouslyToggled = false
|
||||
for i in (0 ..< state.toggledMessageIds.count) {
|
||||
if let messageId = state.toggledMessageIds[i].first {
|
||||
if messageId == message.id {
|
||||
previouslyToggled = true
|
||||
updatedToggledMessageIds = Array(state.toggledMessageIds.prefix(i + 1))
|
||||
|
||||
let messageIdsToToggle = Array(state.toggledMessageIds.suffix(state.toggledMessageIds.count - i - 1)).flatMap { $0 }
|
||||
self.chatControllerInteraction.toggleMessagesSelection(messageIdsToToggle, !state.selecting)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !previouslyToggled {
|
||||
updatedToggledMessageIds = state.toggledMessageIds
|
||||
let isSelected = self.chatControllerInteraction.selectionState?.selectedIds.contains(message.id) ?? false
|
||||
if state.selecting != isSelected {
|
||||
updatedToggledMessageIds.append([message.id])
|
||||
self.chatControllerInteraction.toggleMessagesSelection([message.id], state.selecting)
|
||||
}
|
||||
}
|
||||
|
||||
self.selectionPanState = (state.selecting, state.initialMessageId, updatedToggledMessageIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
guard hasState else {
|
||||
return
|
||||
}
|
||||
let scrollingAreaHeight: CGFloat = 50.0
|
||||
if location.y < scrollingAreaHeight || location.y > self.frame.height - scrollingAreaHeight {
|
||||
if location.y < self.frame.height / 2.0 {
|
||||
self.selectionScrollDelta = (scrollingAreaHeight - location.y) / scrollingAreaHeight
|
||||
} else {
|
||||
self.selectionScrollDelta = -(scrollingAreaHeight - min(scrollingAreaHeight, max(0.0, (self.frame.height - location.y)))) / scrollingAreaHeight
|
||||
}
|
||||
if let displayLink = self.selectionScrollDisplayLink {
|
||||
displayLink.isPaused = false
|
||||
} else {
|
||||
if let _ = self.selectionScrollActivationTimer {
|
||||
} else {
|
||||
let timer = SwiftSignalKit.Timer(timeout: 0.45, repeat: false, completion: { [weak self] in
|
||||
self?.setupSelectionScrolling()
|
||||
}, queue: .mainQueue())
|
||||
timer.start()
|
||||
self.selectionScrollActivationTimer = timer
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.selectionScrollDisplayLink?.isPaused = true
|
||||
self.selectionScrollActivationTimer?.invalidate()
|
||||
self.selectionScrollActivationTimer = nil
|
||||
}
|
||||
}
|
||||
|
||||
private var selectionScrollSkipUpdate = false
|
||||
private func setupSelectionScrolling() {
|
||||
self.selectionScrollDisplayLink = ConstantDisplayLinkAnimator(update: { [weak self] in
|
||||
self?.selectionScrollActivationTimer = nil
|
||||
if let strongSelf = self, let delta = strongSelf.selectionScrollDelta {
|
||||
let distance: CGFloat = 15.0 * min(1.0, 0.15 + abs(delta * delta))
|
||||
let direction: ListViewScrollDirection = delta > 0.0 ? .up : .down
|
||||
let _ = strongSelf.itemGrid.scrollWithDelta(direction == .up ? -distance : distance)
|
||||
|
||||
if let location = strongSelf.selectionLastLocation {
|
||||
if !strongSelf.selectionScrollSkipUpdate {
|
||||
strongSelf.handlePanSelection(location: location)
|
||||
}
|
||||
strongSelf.selectionScrollSkipUpdate = !strongSelf.selectionScrollSkipUpdate
|
||||
}
|
||||
}
|
||||
})
|
||||
self.selectionScrollDisplayLink?.isPaused = false
|
||||
}
|
||||
|
||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
let location = gestureRecognizer.location(in: gestureRecognizer.view)
|
||||
@ -2717,3 +2862,65 @@ func updateVisualMediaStoredState(engine: TelegramEngine, peerId: PeerId, messag
|
||||
return engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.visualMediaStoredState, id: key)
|
||||
}
|
||||
}
|
||||
|
||||
private class MediaListSelectionRecognizer: UIPanGestureRecognizer {
|
||||
private let selectionGestureActivationThreshold: CGFloat = 5.0
|
||||
|
||||
var recognized: Bool? = nil
|
||||
var initialLocation: CGPoint = CGPoint()
|
||||
|
||||
public var shouldBegin: (() -> Bool)?
|
||||
|
||||
public override init(target: Any?, action: Selector?) {
|
||||
super.init(target: target, action: action)
|
||||
|
||||
self.minimumNumberOfTouches = 2
|
||||
self.maximumNumberOfTouches = 2
|
||||
}
|
||||
|
||||
public override func reset() {
|
||||
super.reset()
|
||||
|
||||
self.recognized = nil
|
||||
}
|
||||
|
||||
public override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
if let shouldBegin = self.shouldBegin, !shouldBegin() {
|
||||
self.state = .failed
|
||||
} else {
|
||||
let touch = touches.first!
|
||||
self.initialLocation = touch.location(in: self.view)
|
||||
}
|
||||
}
|
||||
|
||||
public override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
let location = touches.first!.location(in: self.view)
|
||||
let translation = location.offsetBy(dx: -self.initialLocation.x, dy: -self.initialLocation.y)
|
||||
|
||||
let touchesArray = Array(touches)
|
||||
if self.recognized == nil, touchesArray.count == 2 {
|
||||
if let firstTouch = touchesArray.first, let secondTouch = touchesArray.last {
|
||||
let firstLocation = firstTouch.location(in: self.view)
|
||||
let secondLocation = secondTouch.location(in: self.view)
|
||||
|
||||
func distance(_ v1: CGPoint, _ v2: CGPoint) -> CGFloat {
|
||||
let dx = v1.x - v2.x
|
||||
let dy = v1.y - v2.y
|
||||
return sqrt(dx * dx + dy * dy)
|
||||
}
|
||||
if distance(firstLocation, secondLocation) > 200.0 {
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
if self.state != .failed && (abs(translation.y) >= selectionGestureActivationThreshold) {
|
||||
self.recognized = true
|
||||
}
|
||||
}
|
||||
|
||||
if let recognized = self.recognized, recognized {
|
||||
super.touchesMoved(touches, with: event)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1044,7 +1044,7 @@ final class PeerInfoAvatarListNode: ASDisplayNode {
|
||||
self.bottomCoverNode = ASDisplayNode()
|
||||
self.bottomCoverNode.backgroundColor = .black
|
||||
|
||||
self.maskNode = DynamicIslandMaskNode(size: CGSize(width: 512.0, height: 512.0))
|
||||
self.maskNode = DynamicIslandMaskNode()
|
||||
self.pinchSourceNode = PinchSourceContainerNode()
|
||||
|
||||
self.avatarContainerNode = PeerInfoAvatarTransformContainerNode(context: context)
|
||||
@ -1125,8 +1125,9 @@ final class PeerInfoAvatarListNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
func update(size: CGSize, avatarSize: CGFloat, isExpanded: Bool, peer: Peer?, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, theme: PresentationTheme, transition: ContainedViewLayoutTransition) {
|
||||
func update(size: CGSize, avatarSize: CGFloat, isExpanded: Bool, peer: Peer?, isForum: Bool, threadId: Int64?, threadInfo: EngineMessageHistoryThread.Info?, theme: PresentationTheme, transition: ContainedViewLayoutTransition) {
|
||||
self.arguments = (peer, threadId, threadInfo, theme, avatarSize, isExpanded)
|
||||
self.maskNode.isForum = isForum
|
||||
self.pinchSourceNode.update(size: size, transition: transition)
|
||||
self.pinchSourceNode.frame = CGRect(origin: CGPoint(), size: size)
|
||||
self.avatarContainerNode.update(peer: peer, threadId: threadId, threadInfo: threadInfo, item: self.item, theme: theme, avatarSize: avatarSize, isExpanded: isExpanded, isSettings: self.isSettings)
|
||||
@ -2579,7 +2580,6 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
self.presentationData = presentationData
|
||||
|
||||
let premiumConfiguration = PremiumConfiguration.with(appConfiguration: self.context.currentAppConfiguration.with { $0 })
|
||||
|
||||
let credibilityIcon: CredibilityIcon
|
||||
if let peer = peer {
|
||||
if peer.isFake {
|
||||
@ -2599,6 +2599,11 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
credibilityIcon = .none
|
||||
}
|
||||
|
||||
var isForum = false
|
||||
if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) {
|
||||
isForum = true
|
||||
}
|
||||
|
||||
if themeUpdated || self.currentCredibilityIcon != credibilityIcon {
|
||||
self.currentCredibilityIcon = credibilityIcon
|
||||
|
||||
@ -2742,7 +2747,11 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
transitionSourceAvatarFrame = avatarNavigationNode.avatarNode.view.convert(avatarNavigationNode.avatarNode.view.bounds, to: navigationTransition.sourceNavigationBar.view)
|
||||
}
|
||||
} else {
|
||||
transitionSourceAvatarFrame = avatarFrame.offsetBy(dx: 0.0, dy: -avatarFrame.maxY).insetBy(dx: avatarSize * 0.4, dy: avatarSize * 0.4)
|
||||
if deviceMetrics.hasDynamicIsland {
|
||||
transitionSourceAvatarFrame = CGRect(origin: CGPoint(x: avatarFrame.minX, y: -20.0), size: avatarFrame.size).insetBy(dx: avatarSize * 0.4, dy: avatarSize * 0.4)
|
||||
} else {
|
||||
transitionSourceAvatarFrame = avatarFrame.offsetBy(dx: 0.0, dy: -avatarFrame.maxY).insetBy(dx: avatarSize * 0.4, dy: avatarSize * 0.4)
|
||||
}
|
||||
}
|
||||
transitionSourceTitleFrame = navigationTransition.sourceTitleFrame
|
||||
transitionSourceSubtitleFrame = navigationTransition.sourceSubtitleFrame
|
||||
@ -3207,12 +3216,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
transition.updateAlpha(node: subtitleArrowNode, alpha: (1.0 - titleCollapseFraction))
|
||||
}
|
||||
}
|
||||
|
||||
var isForum = false
|
||||
if let channel = peer as? TelegramChannel, channel.flags.contains(.isForum) {
|
||||
isForum = true
|
||||
}
|
||||
|
||||
|
||||
let avatarCornerRadius: CGFloat = isForum ? floor(avatarSize * 0.25) : avatarSize / 2.0
|
||||
|
||||
if self.isAvatarExpanded {
|
||||
@ -3240,7 +3244,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
})
|
||||
}
|
||||
|
||||
self.avatarListNode.update(size: CGSize(), avatarSize: avatarSize, isExpanded: self.isAvatarExpanded, peer: peer, threadId: self.forumTopicThreadId, threadInfo: threadData?.info, theme: presentationData.theme, transition: transition)
|
||||
self.avatarListNode.update(size: CGSize(), avatarSize: avatarSize, isExpanded: self.isAvatarExpanded, peer: peer, isForum: isForum, threadId: self.forumTopicThreadId, threadInfo: threadData?.info, theme: presentationData.theme, transition: transition)
|
||||
self.editingContentNode.avatarNode.update(peer: peer, threadData: threadData, chatLocation: self.chatLocation, item: self.avatarListNode.item, updatingAvatar: state.updatingAvatar, uploadProgress: state.avatarUploadProgress, theme: presentationData.theme, avatarSize: avatarSize, isEditing: state.isEditing)
|
||||
self.avatarOverlayNode.update(peer: peer, threadData: threadData, chatLocation: self.chatLocation, item: self.avatarListNode.item, updatingAvatar: state.updatingAvatar, uploadProgress: state.avatarUploadProgress, theme: presentationData.theme, avatarSize: avatarSize, isEditing: state.isEditing)
|
||||
if additive {
|
||||
@ -3305,7 +3309,7 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
transition.updateSublayerTransformScale(node: self.avatarListNode.listContainerTransformNode, scale: avatarListContainerScale)
|
||||
}
|
||||
|
||||
if deviceMetrics.hasDynamicIsland && !isForum && self.forumTopicThreadId == nil {
|
||||
if deviceMetrics.hasDynamicIsland && self.forumTopicThreadId == nil {
|
||||
self.avatarListNode.maskNode.frame = CGRect(origin: CGPoint(x: -85.5, y: -self.avatarListNode.frame.minY + 48.0), size: CGSize(width: 171.0, height: 171.0))
|
||||
self.avatarListNode.bottomCoverNode.frame = self.avatarListNode.maskNode.frame
|
||||
self.avatarListNode.topCoverNode.frame = self.avatarListNode.maskNode.frame
|
||||
@ -3674,17 +3678,35 @@ final class PeerInfoHeaderNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
private class DynamicIslandMaskNode: ManagedAnimationNode {
|
||||
var frameIndex: Int = 0
|
||||
private class DynamicIslandMaskNode: ASDisplayNode {
|
||||
private var animationNode: AnimationNode?
|
||||
|
||||
var isForum = false {
|
||||
didSet {
|
||||
if self.isForum != oldValue {
|
||||
self.animationNode?.removeFromSupernode()
|
||||
let animationNode = AnimationNode(animation: "ForumAvatarMask")
|
||||
self.addSubnode(animationNode)
|
||||
self.animationNode = animationNode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override init() {
|
||||
let animationNode = AnimationNode(animation: "UserAvatarMask")
|
||||
self.animationNode = animationNode
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(animationNode)
|
||||
}
|
||||
|
||||
func update(_ value: CGFloat) {
|
||||
let lowerBound = 0
|
||||
let upperBound = 180
|
||||
let frameIndex = lowerBound + Int(value * CGFloat(upperBound - lowerBound))
|
||||
if frameIndex != self.frameIndex {
|
||||
self.frameIndex = frameIndex
|
||||
self.trackTo(item: ManagedAnimationItem(source: .local("UserAvatarMask"), frames: .range(startFrame: frameIndex, endFrame: frameIndex), duration: 0.001))
|
||||
}
|
||||
self.animationNode?.setProgress(value)
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
self.animationNode?.frame = self.bounds
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3054,7 +3054,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, UIScrollViewDelegate
|
||||
let controller = ForumCreateTopicScreen(context: strongSelf.context, peerId: strongSelf.peerId, mode: .edit(threadId: threadId, threadInfo: threadData.info, isHidden: threadData.isHidden))
|
||||
controller.navigationPresentation = .modal
|
||||
let context = strongSelf.context
|
||||
controller.completion = { [weak controller] title, fileId, isHidden in
|
||||
controller.completion = { [weak controller] title, fileId, _, isHidden in
|
||||
let _ = (context.engine.peers.editForumChannelTopic(id: peerId, threadId: threadId, title: title, iconFileId: fileId)
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
controller?.dismiss()
|
||||
|
@ -60,7 +60,7 @@ final class MessageMediaPlaylistItem: SharedMediaPlaylistItem {
|
||||
return MessageMediaPlaylistItemStableId(stableId: message.stableId)
|
||||
}
|
||||
|
||||
var playbackData: SharedMediaPlaybackData? {
|
||||
lazy var playbackData: SharedMediaPlaybackData? = {
|
||||
if let file = extractFileMedia(self.message) {
|
||||
let fileReference = FileMediaReference.message(message: MessageReference(self.message), media: file)
|
||||
let source = SharedMediaPlaybackDataSource.telegramFile(reference: fileReference, isCopyProtected: self.message.isCopyProtected())
|
||||
@ -93,9 +93,9 @@ final class MessageMediaPlaylistItem: SharedMediaPlaylistItem {
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
|
||||
var displayData: SharedMediaPlaybackDisplayData? {
|
||||
lazy var displayData: SharedMediaPlaybackDisplayData? = {
|
||||
if let file = extractFileMedia(self.message) {
|
||||
let text = self.message.text
|
||||
var entities: [MessageTextEntity] = []
|
||||
@ -108,40 +108,42 @@ final class MessageMediaPlaylistItem: SharedMediaPlaylistItem {
|
||||
|
||||
for attribute in file.attributes {
|
||||
switch attribute {
|
||||
case let .Audio(isVoice, duration, title, performer, _):
|
||||
if isVoice {
|
||||
return SharedMediaPlaybackDisplayData.voice(author: self.message.effectiveAuthor, peer: self.message.peers[self.message.id.peerId])
|
||||
} else {
|
||||
var updatedTitle = title
|
||||
let updatedPerformer = performer
|
||||
if (title ?? "").isEmpty && (performer ?? "").isEmpty {
|
||||
updatedTitle = file.fileName ?? ""
|
||||
}
|
||||
|
||||
let albumArt: SharedMediaPlaybackAlbumArt?
|
||||
if file.fileName?.lowercased().hasSuffix(".ogg") == true {
|
||||
albumArt = nil
|
||||
} else {
|
||||
albumArt = SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(file: .message(message: MessageReference(self.message), media: file), title: updatedTitle ?? "", performer: updatedPerformer ?? "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(file: .message(message: MessageReference(self.message), media: file), title: updatedTitle ?? "", performer: updatedPerformer ?? "", isThumbnail: false))
|
||||
}
|
||||
|
||||
return SharedMediaPlaybackDisplayData.music(title: updatedTitle, performer: updatedPerformer, albumArt: albumArt, long: CGFloat(duration) > 10.0 * 60.0, caption: caption)
|
||||
case let .Audio(isVoice, duration, title, performer, _):
|
||||
let displayData: SharedMediaPlaybackDisplayData
|
||||
if isVoice {
|
||||
displayData = SharedMediaPlaybackDisplayData.voice(author: self.message.effectiveAuthor, peer: self.message.peers[self.message.id.peerId])
|
||||
} else {
|
||||
var updatedTitle = title
|
||||
let updatedPerformer = performer
|
||||
if (title ?? "").isEmpty && (performer ?? "").isEmpty {
|
||||
updatedTitle = file.fileName ?? ""
|
||||
}
|
||||
case let .Video(_, _, flags):
|
||||
if flags.contains(.instantRoundVideo) {
|
||||
return SharedMediaPlaybackDisplayData.instantVideo(author: self.message.effectiveAuthor, peer: self.message.peers[self.message.id.peerId], timestamp: self.message.timestamp)
|
||||
|
||||
let albumArt: SharedMediaPlaybackAlbumArt?
|
||||
if file.fileName?.lowercased().hasSuffix(".ogg") == true {
|
||||
albumArt = nil
|
||||
} else {
|
||||
return nil
|
||||
albumArt = SharedMediaPlaybackAlbumArt(thumbnailResource: ExternalMusicAlbumArtResource(file: .message(message: MessageReference(self.message), media: file), title: updatedTitle ?? "", performer: updatedPerformer ?? "", isThumbnail: true), fullSizeResource: ExternalMusicAlbumArtResource(file: .message(message: MessageReference(self.message), media: file), title: updatedTitle ?? "", performer: updatedPerformer ?? "", isThumbnail: false))
|
||||
}
|
||||
default:
|
||||
break
|
||||
|
||||
displayData = SharedMediaPlaybackDisplayData.music(title: updatedTitle, performer: updatedPerformer, albumArt: albumArt, long: CGFloat(duration) > 10.0 * 60.0, caption: caption)
|
||||
}
|
||||
return displayData
|
||||
case let .Video(_, _, flags):
|
||||
if flags.contains(.instantRoundVideo) {
|
||||
return SharedMediaPlaybackDisplayData.instantVideo(author: self.message.effectiveAuthor, peer: self.message.peers[self.message.id.peerId], timestamp: self.message.timestamp)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return SharedMediaPlaybackDisplayData.music(title: file.fileName ?? "", performer: self.message.effectiveAuthor?.debugDisplayTitle ?? "", albumArt: nil, long: false, caption: caption)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
private enum NavigatedMessageFromViewPosition {
|
||||
|
@ -22,6 +22,7 @@ import AnimatedStickerNode
|
||||
import TelegramAnimatedStickerNode
|
||||
import SolidRoundedButtonNode
|
||||
import ContextUI
|
||||
import TextFormat
|
||||
|
||||
final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
private let context: AccountContext
|
||||
@ -611,19 +612,28 @@ final class PeerSelectionControllerNode: ASDisplayNode {
|
||||
}, openLinkEditing: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
var selectionRange: Range<Int>?
|
||||
var text: String?
|
||||
var text: NSAttributedString?
|
||||
var inputMode: ChatInputMode?
|
||||
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, { state in
|
||||
selectionRange = state.interfaceState.effectiveInputState.selectionRange
|
||||
if let selectionRange = selectionRange {
|
||||
text = state.interfaceState.effectiveInputState.inputText.attributedSubstring(from: NSRange(location: selectionRange.startIndex, length: selectionRange.count)).string
|
||||
text = state.interfaceState.effectiveInputState.inputText.attributedSubstring(from: NSRange(location: selectionRange.startIndex, length: selectionRange.count))
|
||||
}
|
||||
inputMode = state.inputMode
|
||||
return state
|
||||
})
|
||||
|
||||
let controller = chatTextLinkEditController(sharedContext: strongSelf.context.sharedContext, updatedPresentationData: (presentationData, .never()), account: strongSelf.context.account, text: text ?? "", link: nil, apply: { [weak self] link in
|
||||
var link: String?
|
||||
if let text {
|
||||
text.enumerateAttributes(in: NSMakeRange(0, text.length)) { attributes, _, _ in
|
||||
if let linkAttribute = attributes[ChatTextInputAttributes.textUrl] as? ChatTextInputTextUrlAttribute {
|
||||
link = linkAttribute.url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let controller = chatTextLinkEditController(sharedContext: strongSelf.context.sharedContext, updatedPresentationData: (presentationData, .never()), account: strongSelf.context.account, text: text?.string ?? "", link: link, apply: { [weak self] link in
|
||||
if let strongSelf = self, let inputMode = inputMode, let selectionRange = selectionRange {
|
||||
if let link = link {
|
||||
strongSelf.updateChatPresentationInterfaceState(animated: true, { state in
|
||||
|
@ -374,6 +374,9 @@ final class SharedMediaPlayer {
|
||||
strongSelf.forceAudioToSpeaker = forceAudioToSpeaker
|
||||
strongSelf.playbackItem?.setForceAudioToSpeaker(forceAudioToSpeaker)
|
||||
if !forceAudioToSpeaker {
|
||||
if let playbackStateValue = strongSelf._playbackStateValue, case let .item(item) = playbackStateValue, item.status.timestamp < 1.5 {
|
||||
strongSelf.control(.seek(0.0))
|
||||
}
|
||||
strongSelf.control(.playback(.play))
|
||||
} else {
|
||||
strongSelf.control(.playback(.pause))
|
||||
|
@ -122,6 +122,9 @@ public final class WebSearchController: ViewController {
|
||||
|
||||
public var attemptItemSelection: (ChatContextResult) -> Bool = { _ in return true }
|
||||
|
||||
private var searchQueryPromise = ValuePromise<String>()
|
||||
private var searchQueryDisposable: Disposable?
|
||||
|
||||
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peer: EnginePeer?, chatLocation: ChatLocation?, configuration: EngineConfiguration.SearchBots, mode: WebSearchControllerMode, activateOnDisplay: Bool = true) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
@ -195,7 +198,7 @@ public final class WebSearchController: ViewController {
|
||||
self.navigationContentNode = navigationContentNode
|
||||
navigationContentNode.setQueryUpdated { [weak self] query in
|
||||
if let strongSelf = self, strongSelf.isNodeLoaded {
|
||||
strongSelf.updateSearchQuery(query)
|
||||
strongSelf.searchQueryPromise.set(query)
|
||||
strongSelf.searchingUpdated(!query.isEmpty)
|
||||
}
|
||||
}
|
||||
@ -288,6 +291,24 @@ public final class WebSearchController: ViewController {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
let throttledSearchQuery = self.searchQueryPromise.get()
|
||||
|> mapToSignal { query -> Signal<String, NoError> in
|
||||
if !query.isEmpty {
|
||||
return (.complete() |> delay(1.0, queue: Queue.mainQueue()))
|
||||
|> then(.single(query))
|
||||
} else {
|
||||
return .single(query)
|
||||
}
|
||||
}
|
||||
|
||||
self.searchQueryDisposable = (throttledSearchQuery
|
||||
|> deliverOnMainQueue).start(next: { [weak self] query in
|
||||
if let self {
|
||||
self.updateSearchQuery(query)
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
@ -298,6 +319,7 @@ public final class WebSearchController: ViewController {
|
||||
self.disposable?.dispose()
|
||||
self.resultsDisposable.dispose()
|
||||
self.selectionDisposable?.dispose()
|
||||
self.searchQueryDisposable?.dispose()
|
||||
}
|
||||
|
||||
public func cancel() {
|
||||
|
@ -61,7 +61,7 @@
|
||||
|
||||
_dimensions = CGSizeMake(width, height);
|
||||
|
||||
if ((_frameRate > 60) || _animation->duration() > 7.0) {
|
||||
if ((_frameRate > 60) || _animation->duration() > 9.0) {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user