Merge commit '38f6adeda1758d0f605000bc4356688c48768c0b'

This commit is contained in:
Ali 2023-03-24 01:22:18 +04:00
commit 16a1ac35db
52 changed files with 893 additions and 502 deletions

View File

@ -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

Binary file not shown.

View File

@ -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";

View File

@ -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",
],

View File

@ -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) {

View File

@ -36,6 +36,7 @@ swift_library(
"//submodules/Components/AnimatedStickerComponent:AnimatedStickerComponent",
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
"//submodules/ShimmerEffect:ShimmerEffect",
"//submodules/TextFormat:TextFormat",
],
visibility = [
"//visibility:public",

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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
}

View File

@ -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 {

View File

@ -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

View File

@ -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

View File

@ -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)"

View File

@ -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()

View File

@ -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 {

View File

@ -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:

View File

@ -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) {

View File

@ -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
}

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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 {

View File

@ -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)
}

View File

@ -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)";

View File

@ -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 }

View File

@ -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

View File

@ -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 {

View File

@ -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)
}))
}

View File

@ -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 {

View File

@ -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,

View File

@ -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))

View File

@ -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)

View File

@ -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)

View File

@ -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(

View File

@ -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)
}
}

View File

@ -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, *)

View File

@ -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))
})
}
}

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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()

View File

@ -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 {

View File

@ -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

View File

@ -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))

View File

@ -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() {

View File

@ -61,7 +61,7 @@
_dimensions = CGSizeMake(width, height);
if ((_frameRate > 60) || _animation->duration() > 7.0) {
if ((_frameRate > 60) || _animation->duration() > 9.0) {
return nil;
}
}