mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-01 16:06:59 +00:00
Merge branch 'missing-avatar'
This commit is contained in:
commit
72773d7ecb
@ -4637,6 +4637,12 @@ Sorry for the inconvenience.";
|
||||
"VoiceOver.Chat.FileFrom" = "File, from: %@";
|
||||
"VoiceOver.Chat.File" = "File";
|
||||
"VoiceOver.Chat.YourFile" = "Your file";
|
||||
"VoiceOver.Chat.StickerFrom" = "Sticker, from: %@";
|
||||
"VoiceOver.Chat.Sticker" = "Sticker";
|
||||
"VoiceOver.Chat.YourSticker" = "Your sticker";
|
||||
"VoiceOver.Chat.AnimatedStickerFrom" = "Animated sticker, from: %@";
|
||||
"VoiceOver.Chat.AnimatedSticker" = "Animated sticker";
|
||||
"VoiceOver.Chat.YourAnimatedSticker" = "Your animated sticker";
|
||||
"VoiceOver.Chat.ContactFrom" = "Shared contact, from: %@";
|
||||
"VoiceOver.Chat.Contact" = "Shared contact";
|
||||
"VoiceOver.Chat.ContactPhoneNumberCount_1" = "%@ phone number";
|
||||
@ -4677,6 +4683,16 @@ Sorry for the inconvenience.";
|
||||
"VoiceOver.MessageContextReply" = "Reply";
|
||||
"VoiceOver.MessageContextOpenMessageMenu" = "Open message menu";
|
||||
|
||||
"VoiceOver.Keyboard" = "Keyboard";
|
||||
"VoiceOver.Stickers" = "Stickers";
|
||||
"VoiceOver.ScheduledMessages" = "Scheduled Messages";
|
||||
"VoiceOver.BotCommands" = "Bot Commands";
|
||||
"VoiceOver.BotKeyboard" = "Bot Keyboard";
|
||||
"VoiceOver.SilentPostOn" = "Silent Broadcast On";
|
||||
"VoiceOver.SilentPostOff" = "Silent Broadcast Off";
|
||||
"VoiceOver.SelfDestructTimerOn" = "Self-destruct Timer: %@";
|
||||
"VoiceOver.SelfDestructTimerOff" = "Self-destruct Timer Off";
|
||||
|
||||
"ProxyServer.VoiceOver.Active" = "Active";
|
||||
|
||||
"Conversation.ScheduleMessage.Title" = "Schedule Message";
|
||||
@ -6158,3 +6174,13 @@ Sorry for the inconvenience.";
|
||||
|
||||
"Message.ScamAccount" = "Scam";
|
||||
"Message.FakeAccount" = "Fake";
|
||||
|
||||
"InstantPage.FontSanFrancisco" = "San Francisco";
|
||||
"InstantPage.FontNewYork" = "New York";
|
||||
"InstantPage.Search" = "Search";
|
||||
"InstantPage.OpenInBrowser" = "Open in %@";
|
||||
"InstantPage.VoiceOver.IncreaseFontSize" = "Increase Font Size";
|
||||
"InstantPage.VoiceOver.DecreaseFontSize" = "Decrease Font Size";
|
||||
"InstantPage.VoiceOver.ResetFontSize" = "Reset Font Size";
|
||||
|
||||
"Contacts.VoiceOver.AddContact" = "Add Contact";
|
||||
|
@ -50,7 +50,10 @@ final class BotPaymentFieldItemNode: BotPaymentItemNode, UITextFieldDelegate {
|
||||
switch contentType {
|
||||
case .generic:
|
||||
break
|
||||
case .name, .address:
|
||||
case .name:
|
||||
self.textField.textField.autocorrectionType = .no
|
||||
self.textField.textField.keyboardType = .asciiCapable
|
||||
case .address:
|
||||
self.textField.textField.autocorrectionType = .no
|
||||
case .phoneNumber:
|
||||
self.textField.textField.keyboardType = .phonePad
|
||||
|
@ -600,7 +600,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
|> take(1)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] greetingSticker in
|
||||
if let strongSelf = self {
|
||||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), activateInput: activateInput, scrollToEndIfExists: scrollToEndIfExists, greetingData: greetingSticker.flatMap({ ChatGreetingData(sticker: $0) }), animated: !scrollToEndIfExists, options: strongSelf.groupId == PeerGroupId.root ? [.removeOnMasterDetails] : [], parentGroupId: strongSelf.groupId, completion: { [weak self] controller in
|
||||
strongSelf.context.sharedContext.navigateToChatController(NavigateToChatControllerParams(navigationController: navigationController, context: strongSelf.context, chatLocation: .peer(peer.id), activateInput: activateInput && !peer.isDeleted, scrollToEndIfExists: scrollToEndIfExists, greetingData: greetingSticker.flatMap({ ChatGreetingData(sticker: $0) }), animated: !scrollToEndIfExists, options: strongSelf.groupId == PeerGroupId.root ? [.removeOnMasterDetails] : [], parentGroupId: strongSelf.groupId, completion: { [weak self] controller in
|
||||
self?.chatListDisplayNode.containerNode.currentItemNode.clearHighlightAnimated(true)
|
||||
if let promoInfo = promoInfo {
|
||||
switch promoInfo {
|
||||
|
@ -142,6 +142,8 @@ private final class ItemNode: ASDisplayNode {
|
||||
|
||||
super.init()
|
||||
|
||||
self.isAccessibilityElement = true
|
||||
|
||||
self.extractedContainerNode.contentNode.addSubnode(self.extractedBackgroundNode)
|
||||
self.extractedContainerNode.contentNode.addSubnode(self.titleContainer)
|
||||
self.titleContainer.addSubnode(self.titleNode)
|
||||
@ -495,6 +497,7 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
|
||||
super.init()
|
||||
|
||||
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||
self.scrollNode.view.scrollsToTop = false
|
||||
self.scrollNode.view.delaysContentTouches = false
|
||||
self.scrollNode.view.canCancelContentTouches = true
|
||||
|
@ -82,6 +82,7 @@ public class ContactsController: ViewController {
|
||||
private var presentationDataDisposable: Disposable?
|
||||
private var authorizationDisposable: Disposable?
|
||||
private let sortOrderPromise = Promise<ContactsSortOrder>()
|
||||
private let isInVoiceOver = ValuePromise<Bool>(false)
|
||||
|
||||
private var searchContentNode: NavigationBarSearchContentNode?
|
||||
|
||||
@ -117,6 +118,7 @@ public class ContactsController: ViewController {
|
||||
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationAddIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.addPressed))
|
||||
self.navigationItem.rightBarButtonItem?.accessibilityLabel = self.presentationData.strings.Contacts_VoiceOver_AddContact
|
||||
|
||||
self.scrollToTop = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
@ -199,11 +201,20 @@ public class ContactsController: ViewController {
|
||||
self.navigationItem.backBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Back, style: .plain, target: nil, action: nil)
|
||||
if self.navigationItem.rightBarButtonItem != nil {
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationAddIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.addPressed))
|
||||
self.navigationItem.rightBarButtonItem?.accessibilityLabel = self.presentationData.strings.Contacts_VoiceOver_AddContact
|
||||
}
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = ContactsControllerNode(context: self.context, sortOrder: sortOrderPromise.get() |> distinctUntilChanged, present: { [weak self] c, a in
|
||||
let sortOrderSignal: Signal<ContactsSortOrder, NoError> = combineLatest(self.sortOrderPromise.get(), self.isInVoiceOver.get())
|
||||
|> map { sortOrder, isInVoiceOver in
|
||||
if isInVoiceOver {
|
||||
return .natural
|
||||
} else {
|
||||
return sortOrder
|
||||
}
|
||||
}
|
||||
self.displayNode = ContactsControllerNode(context: self.context, sortOrder: sortOrderSignal |> distinctUntilChanged, present: { [weak self] c, a in
|
||||
self?.present(c, in: .window(.root), with: a)
|
||||
}, controller: self)
|
||||
self._ready.set(self.contactsNode.contactListNode.ready)
|
||||
@ -404,6 +415,8 @@ public class ContactsController: ViewController {
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
self.isInVoiceOver.set(layout.inVoiceOver)
|
||||
|
||||
self.validLayout = layout
|
||||
|
||||
self.contactsNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationInsetHeight, actualNavigationBarHeight: self.navigationHeight, transition: transition)
|
||||
|
@ -124,6 +124,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
private let dimNode: ASDisplayNode
|
||||
private let withoutBlurDimNode: ASDisplayNode
|
||||
private let dismissNode: ASDisplayNode
|
||||
private let dismissAccessibilityArea: AccessibilityAreaNode
|
||||
|
||||
private let clippingNode: ASDisplayNode
|
||||
private let scrollNode: ASScrollNode
|
||||
@ -184,9 +185,9 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
self.withoutBlurDimNode.alpha = 0.0
|
||||
|
||||
self.dismissNode = ASDisplayNode()
|
||||
self.dismissNode.isAccessibilityElement = true
|
||||
self.dismissNode.accessibilityLabel = presentationData.strings.VoiceOver_DismissContextMenu
|
||||
self.dismissNode.accessibilityTraits = .button
|
||||
self.dismissAccessibilityArea = AccessibilityAreaNode()
|
||||
self.dismissAccessibilityArea.accessibilityLabel = presentationData.strings.VoiceOver_DismissContextMenu
|
||||
self.dismissAccessibilityArea.accessibilityTraits = .button
|
||||
|
||||
self.clippingNode = ASDisplayNode()
|
||||
self.clippingNode.clipsToBounds = true
|
||||
@ -242,6 +243,7 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
|
||||
self.clippingNode.addSubnode(self.scrollNode)
|
||||
self.scrollNode.addSubnode(self.dismissNode)
|
||||
self.scrollNode.addSubnode(self.dismissAccessibilityArea)
|
||||
|
||||
self.scrollNode.addSubnode(self.actionsContainerNode)
|
||||
self.reactionContextNode.flatMap(self.addSubnode)
|
||||
@ -439,6 +441,11 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
}
|
||||
|
||||
self.initializeContent()
|
||||
|
||||
self.dismissAccessibilityArea.activate = { [weak self] in
|
||||
self?.dimNodeTapped()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
@ -1406,7 +1413,8 @@ private final class ContextControllerNode: ViewControllerTracingNode, UIScrollVi
|
||||
}
|
||||
}
|
||||
|
||||
transition.updateFrame(node: self.dismissNode, frame: CGRect(origin: CGPoint(), size: scrollNode.view.contentSize))
|
||||
transition.updateFrame(node: self.dismissNode, frame: CGRect(origin: CGPoint(), size: self.scrollNode.view.contentSize))
|
||||
self.dismissAccessibilityArea.frame = CGRect(origin: CGPoint(), size: self.scrollNode.view.contentSize)
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
@ -1593,6 +1601,7 @@ public final class ContextController: ViewController, StandalonePresentableContr
|
||||
self.statusBar.statusBarStyle = .Hide
|
||||
}
|
||||
self.lockOrientation = true
|
||||
self.blocksBackgroundWhenInOverlay = true
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
|
@ -9,6 +9,7 @@ public struct ImmediateTextNodeLayoutInfo {
|
||||
public class ImmediateTextNode: TextNode {
|
||||
public var attributedText: NSAttributedString?
|
||||
public var textAlignment: NSTextAlignment = .natural
|
||||
public var verticalAlignment: TextVerticalAlignment = .top
|
||||
public var truncationType: CTLineTruncationType = .end
|
||||
public var maximumNumberOfLines: Int = 1
|
||||
public var lineSpacing: CGFloat = 0.0
|
||||
@ -88,7 +89,7 @@ public class ImmediateTextNode: TextNode {
|
||||
self.constrainedSize = constrainedSize
|
||||
|
||||
let makeLayout = TextNode.asyncLayout(self)
|
||||
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets, textShadowColor: self.textShadowColor, textStroke: self.textStroke))
|
||||
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, verticalAlignment: self.verticalAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets, textShadowColor: self.textShadowColor, textStroke: self.textStroke))
|
||||
let _ = apply()
|
||||
if layout.numberOfLines > 1 {
|
||||
self.trailingLineWidth = layout.trailingLineWidth
|
||||
@ -102,7 +103,7 @@ public class ImmediateTextNode: TextNode {
|
||||
self.constrainedSize = constrainedSize
|
||||
|
||||
let makeLayout = TextNode.asyncLayout(self)
|
||||
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets))
|
||||
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, verticalAlignment: self.verticalAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets))
|
||||
let _ = apply()
|
||||
return ImmediateTextNodeLayoutInfo(size: layout.size, truncated: layout.truncated)
|
||||
}
|
||||
@ -111,7 +112,7 @@ public class ImmediateTextNode: TextNode {
|
||||
self.constrainedSize = constrainedSize
|
||||
|
||||
let makeLayout = TextNode.asyncLayout(self)
|
||||
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets))
|
||||
let (layout, apply) = makeLayout(TextNodeLayoutArguments(attributedString: self.attributedText, backgroundColor: nil, maximumNumberOfLines: self.maximumNumberOfLines, truncationType: self.truncationType, constrainedSize: constrainedSize, alignment: self.textAlignment, verticalAlignment: self.verticalAlignment, lineSpacing: self.lineSpacing, cutout: self.cutout, insets: self.insets))
|
||||
let _ = apply()
|
||||
return layout
|
||||
}
|
||||
|
@ -563,7 +563,7 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var previousOverlayContainer: NavigationOverlayContainer?
|
||||
for i in (0 ..< self.overlayContainers.count).reversed() {
|
||||
let overlayContainer = self.overlayContainers[i]
|
||||
@ -1020,7 +1020,21 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
self.statusBarHost?.setStatusBarHidden(statusBarHidden, animated: animateStatusBarStyleTransition)
|
||||
}
|
||||
|
||||
var topHasOpaque = false
|
||||
var foundControllerInFocus = false
|
||||
|
||||
for container in self.globalOverlayContainers.reversed() {
|
||||
let controller = container.controller
|
||||
if topHasOpaque {
|
||||
controller.displayNode.accessibilityElementsHidden = true
|
||||
} else {
|
||||
if controller.isOpaqueWhenInOverlay || controller.blocksBackgroundWhenInOverlay {
|
||||
topHasOpaque = true
|
||||
}
|
||||
controller.displayNode.accessibilityElementsHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
for container in self.overlayContainers.reversed() {
|
||||
if foundControllerInFocus {
|
||||
container.controller.isInFocus = false
|
||||
@ -1028,6 +1042,16 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
foundControllerInFocus = true
|
||||
container.controller.isInFocus = true
|
||||
}
|
||||
|
||||
let controller = container.controller
|
||||
if topHasOpaque {
|
||||
controller.displayNode.accessibilityElementsHidden = true
|
||||
} else {
|
||||
if controller.isOpaqueWhenInOverlay || controller.blocksBackgroundWhenInOverlay {
|
||||
topHasOpaque = true
|
||||
}
|
||||
controller.displayNode.accessibilityElementsHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
for container in self.modalContainers.reversed() {
|
||||
@ -1037,6 +1061,17 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
foundControllerInFocus = true
|
||||
container.container.isInFocus = true
|
||||
}
|
||||
|
||||
if let controller = container.container.controllers.last {
|
||||
if topHasOpaque {
|
||||
controller.displayNode.accessibilityElementsHidden = true
|
||||
} else {
|
||||
if controller.isOpaqueWhenInOverlay || controller.blocksBackgroundWhenInOverlay {
|
||||
topHasOpaque = true
|
||||
}
|
||||
controller.displayNode.accessibilityElementsHidden = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let rootContainer = self.rootContainer {
|
||||
@ -1048,6 +1083,17 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
foundControllerInFocus = true
|
||||
container.isInFocus = true
|
||||
}
|
||||
|
||||
if let controller = container.controllers.last {
|
||||
if topHasOpaque {
|
||||
controller.displayNode.accessibilityElementsHidden = true
|
||||
} else {
|
||||
if controller.isOpaqueWhenInOverlay || controller.blocksBackgroundWhenInOverlay {
|
||||
topHasOpaque = true
|
||||
}
|
||||
controller.displayNode.accessibilityElementsHidden = false
|
||||
}
|
||||
}
|
||||
case let .split(split):
|
||||
if foundControllerInFocus {
|
||||
split.isInFocus = false
|
||||
@ -1055,6 +1101,27 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
foundControllerInFocus = true
|
||||
split.isInFocus = true
|
||||
}
|
||||
|
||||
if let controller = split.masterControllers.last {
|
||||
if topHasOpaque {
|
||||
controller.displayNode.accessibilityElementsHidden = true
|
||||
} else {
|
||||
if controller.isOpaqueWhenInOverlay || controller.blocksBackgroundWhenInOverlay {
|
||||
topHasOpaque = true
|
||||
}
|
||||
controller.displayNode.accessibilityElementsHidden = false
|
||||
}
|
||||
}
|
||||
if let controller = split.detailControllers.last {
|
||||
if topHasOpaque {
|
||||
controller.displayNode.accessibilityElementsHidden = true
|
||||
} else {
|
||||
if controller.isOpaqueWhenInOverlay || controller.blocksBackgroundWhenInOverlay {
|
||||
topHasOpaque = true
|
||||
}
|
||||
controller.displayNode.accessibilityElementsHidden = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,7 @@ private final class NavigationButtonItemNode: ImmediateTextNode {
|
||||
self?.isEnabled = value
|
||||
}
|
||||
self.accessibilityHint = item.accessibilityHint
|
||||
self.accessibilityLabel = item.accessibilityLabel
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -194,6 +195,8 @@ private final class NavigationButtonItemNode: ImmediateTextNode {
|
||||
self.hitTestSlop = UIEdgeInsets(top: -16.0, left: -10.0, bottom: -16.0, right: -10.0)
|
||||
self.displaysAsynchronously = false
|
||||
|
||||
self.verticalAlignment = .middle
|
||||
|
||||
self.accessibilityTraits = .button
|
||||
}
|
||||
|
||||
@ -213,7 +216,7 @@ private final class NavigationButtonItemNode: ImmediateTextNode {
|
||||
}
|
||||
|
||||
override func updateLayout(_ constrainedSize: CGSize) -> CGSize {
|
||||
let superSize = super.updateLayout(constrainedSize)
|
||||
var superSize = super.updateLayout(constrainedSize)
|
||||
|
||||
if let node = self.node {
|
||||
let nodeSize = node.measure(constrainedSize)
|
||||
@ -227,6 +230,8 @@ private final class NavigationButtonItemNode: ImmediateTextNode {
|
||||
imageNode.frame = imageFrame
|
||||
self.imageRippleNode.frame = imageFrame
|
||||
return size
|
||||
} else {
|
||||
superSize.height = max(44.0, superSize.height)
|
||||
}
|
||||
return superSize
|
||||
}
|
||||
|
@ -94,6 +94,12 @@ private func displayLineFrame(frame: CGRect, isRTL: Bool, boundingRect: CGRect,
|
||||
return lineFrame
|
||||
}
|
||||
|
||||
public enum TextVerticalAlignment {
|
||||
case top
|
||||
case middle
|
||||
case bottom
|
||||
}
|
||||
|
||||
public final class TextNodeLayoutArguments {
|
||||
public let attributedString: NSAttributedString?
|
||||
public let backgroundColor: UIColor?
|
||||
@ -102,6 +108,7 @@ public final class TextNodeLayoutArguments {
|
||||
public let truncationType: CTLineTruncationType
|
||||
public let constrainedSize: CGSize
|
||||
public let alignment: NSTextAlignment
|
||||
public let verticalAlignment: TextVerticalAlignment
|
||||
public let lineSpacing: CGFloat
|
||||
public let cutout: TextNodeCutout?
|
||||
public let insets: UIEdgeInsets
|
||||
@ -109,7 +116,7 @@ public final class TextNodeLayoutArguments {
|
||||
public let textShadowColor: UIColor?
|
||||
public let textStroke: (UIColor, CGFloat)?
|
||||
|
||||
public init(attributedString: NSAttributedString?, backgroundColor: UIColor? = nil, minimumNumberOfLines: Int = 0, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment = .natural, lineSpacing: CGFloat = 0.12, cutout: TextNodeCutout? = nil, insets: UIEdgeInsets = UIEdgeInsets(), lineColor: UIColor? = nil, textShadowColor: UIColor? = nil, textStroke: (UIColor, CGFloat)? = nil) {
|
||||
public init(attributedString: NSAttributedString?, backgroundColor: UIColor? = nil, minimumNumberOfLines: Int = 0, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, alignment: NSTextAlignment = .natural, verticalAlignment: TextVerticalAlignment = .top, lineSpacing: CGFloat = 0.12, cutout: TextNodeCutout? = nil, insets: UIEdgeInsets = UIEdgeInsets(), lineColor: UIColor? = nil, textShadowColor: UIColor? = nil, textStroke: (UIColor, CGFloat)? = nil) {
|
||||
self.attributedString = attributedString
|
||||
self.backgroundColor = backgroundColor
|
||||
self.minimumNumberOfLines = minimumNumberOfLines
|
||||
@ -117,6 +124,7 @@ public final class TextNodeLayoutArguments {
|
||||
self.truncationType = truncationType
|
||||
self.constrainedSize = constrainedSize
|
||||
self.alignment = alignment
|
||||
self.verticalAlignment = verticalAlignment
|
||||
self.lineSpacing = lineSpacing
|
||||
self.cutout = cutout
|
||||
self.insets = insets
|
||||
@ -134,6 +142,7 @@ public final class TextNodeLayout: NSObject {
|
||||
fileprivate let constrainedSize: CGSize
|
||||
fileprivate let explicitAlignment: NSTextAlignment
|
||||
fileprivate let resolvedAlignment: NSTextAlignment
|
||||
fileprivate let verticalAlignment: TextVerticalAlignment
|
||||
fileprivate let lineSpacing: CGFloat
|
||||
fileprivate let cutout: TextNodeCutout?
|
||||
fileprivate let insets: UIEdgeInsets
|
||||
@ -148,13 +157,14 @@ public final class TextNodeLayout: NSObject {
|
||||
fileprivate let textStroke: (UIColor, CGFloat)?
|
||||
public let hasRTL: Bool
|
||||
|
||||
fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, explicitAlignment: NSTextAlignment, resolvedAlignment: NSTextAlignment, lineSpacing: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, rawTextSize: CGSize, truncated: Bool, firstLineOffset: CGFloat, lines: [TextNodeLine], blockQuotes: [TextNodeBlockQuote], backgroundColor: UIColor?, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?) {
|
||||
fileprivate init(attributedString: NSAttributedString?, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, constrainedSize: CGSize, explicitAlignment: NSTextAlignment, resolvedAlignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacing: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, size: CGSize, rawTextSize: CGSize, truncated: Bool, firstLineOffset: CGFloat, lines: [TextNodeLine], blockQuotes: [TextNodeBlockQuote], backgroundColor: UIColor?, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?) {
|
||||
self.attributedString = attributedString
|
||||
self.maximumNumberOfLines = maximumNumberOfLines
|
||||
self.truncationType = truncationType
|
||||
self.constrainedSize = constrainedSize
|
||||
self.explicitAlignment = explicitAlignment
|
||||
self.resolvedAlignment = resolvedAlignment
|
||||
self.verticalAlignment = verticalAlignment
|
||||
self.lineSpacing = lineSpacing
|
||||
self.cutout = cutout
|
||||
self.insets = insets
|
||||
@ -823,7 +833,7 @@ public class TextNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
private class func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?) -> TextNodeLayout {
|
||||
private class func calculateLayout(attributedString: NSAttributedString?, minimumNumberOfLines: Int, maximumNumberOfLines: Int, truncationType: CTLineTruncationType, backgroundColor: UIColor?, constrainedSize: CGSize, alignment: NSTextAlignment, verticalAlignment: TextVerticalAlignment, lineSpacingFactor: CGFloat, cutout: TextNodeCutout?, insets: UIEdgeInsets, lineColor: UIColor?, textShadowColor: UIColor?, textStroke: (UIColor, CGFloat)?) -> TextNodeLayout {
|
||||
if let attributedString = attributedString {
|
||||
|
||||
let stringLength = attributedString.length
|
||||
@ -862,7 +872,7 @@ public class TextNode: ASDisplayNode {
|
||||
var maybeTypesetter: CTTypesetter?
|
||||
maybeTypesetter = CTTypesetterCreateWithAttributedString(attributedString as CFAttributedString)
|
||||
if maybeTypesetter == nil {
|
||||
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: resolvedAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textStroke: textStroke)
|
||||
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: resolvedAlignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textStroke: textStroke)
|
||||
}
|
||||
|
||||
let typesetter = maybeTypesetter!
|
||||
@ -1074,9 +1084,9 @@ public class TextNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: resolvedAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), rawTextSize: CGSize(width: ceil(rawLayoutSize.width) + insets.left + insets.right, height: ceil(rawLayoutSize.height) + insets.top + insets.bottom), truncated: truncated, firstLineOffset: firstLineOffset, lines: lines, blockQuotes: blockQuotes, backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textStroke: textStroke)
|
||||
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: resolvedAlignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(width: ceil(layoutSize.width) + insets.left + insets.right, height: ceil(layoutSize.height) + insets.top + insets.bottom), rawTextSize: CGSize(width: ceil(rawLayoutSize.width) + insets.left + insets.right, height: ceil(rawLayoutSize.height) + insets.top + insets.bottom), truncated: truncated, firstLineOffset: firstLineOffset, lines: lines, blockQuotes: blockQuotes, backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textStroke: textStroke)
|
||||
} else {
|
||||
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: alignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textStroke: textStroke)
|
||||
return TextNodeLayout(attributedString: attributedString, maximumNumberOfLines: maximumNumberOfLines, truncationType: truncationType, constrainedSize: constrainedSize, explicitAlignment: alignment, resolvedAlignment: alignment, verticalAlignment: verticalAlignment, lineSpacing: lineSpacingFactor, cutout: cutout, insets: insets, size: CGSize(), rawTextSize: CGSize(), truncated: false, firstLineOffset: 0.0, lines: [], blockQuotes: [], backgroundColor: backgroundColor, lineColor: lineColor, textShadowColor: textShadowColor, textStroke: textStroke)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1129,7 +1139,15 @@ public class TextNode: ASDisplayNode {
|
||||
context.textMatrix = CGAffineTransform(scaleX: 1.0, y: -1.0)
|
||||
|
||||
let alignment = layout.resolvedAlignment
|
||||
let offset = CGPoint(x: layout.insets.left, y: layout.insets.top)
|
||||
var offset = CGPoint(x: layout.insets.left, y: layout.insets.top)
|
||||
switch layout.verticalAlignment {
|
||||
case .top:
|
||||
break
|
||||
case .middle:
|
||||
offset.y = floor((bounds.height - layout.size.height) / 2.0) + layout.insets.top
|
||||
case .bottom:
|
||||
offset.y = floor(bounds.height - layout.size.height) + layout.insets.top
|
||||
}
|
||||
|
||||
for i in 0 ..< layout.lines.count {
|
||||
let line = layout.lines[i]
|
||||
@ -1230,11 +1248,11 @@ public class TextNode: ASDisplayNode {
|
||||
if stringMatch {
|
||||
layout = existingLayout
|
||||
} else {
|
||||
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke)
|
||||
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke)
|
||||
updated = true
|
||||
}
|
||||
} else {
|
||||
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke)
|
||||
layout = TextNode.calculateLayout(attributedString: arguments.attributedString, minimumNumberOfLines: arguments.minimumNumberOfLines, maximumNumberOfLines: arguments.maximumNumberOfLines, truncationType: arguments.truncationType, backgroundColor: arguments.backgroundColor, constrainedSize: arguments.constrainedSize, alignment: arguments.alignment, verticalAlignment: arguments.verticalAlignment, lineSpacingFactor: arguments.lineSpacing, cutout: arguments.cutout, insets: arguments.insets, lineColor: arguments.lineColor, textShadowColor: arguments.textShadowColor, textStroke: arguments.textStroke)
|
||||
updated = true
|
||||
}
|
||||
|
||||
|
@ -931,9 +931,8 @@ typedef enum
|
||||
else
|
||||
_sheetView.menuWidth = referenceSize.width;
|
||||
|
||||
[self repositionMenuWithReferenceSize:referenceSize];
|
||||
|
||||
[_sheetView layoutSubviews];
|
||||
[self repositionMenuWithReferenceSize:referenceSize];
|
||||
}
|
||||
|
||||
[_sheetView didChangeAbsoluteFrame];
|
||||
|
@ -128,6 +128,8 @@ final class PasscodeEntryControllerNode: ASDisplayNode {
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.view.disablesInteractiveKeyboardGestureRecognizer = true
|
||||
|
||||
self.view.insertSubview(self.effectView, at: 0)
|
||||
|
||||
if self.arguments.cancel != nil {
|
||||
|
@ -161,6 +161,11 @@ final class PasscodeSetupControllerNode: ASDisplayNode {
|
||||
transition.updateFrame(node: self.modeButtonNode, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - insets.bottom - 53.0), size: CGSize(width: layout.size.width, height: 44.0)))
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
self.view.disablesInteractiveKeyboardGestureRecognizer = true
|
||||
}
|
||||
|
||||
func updateMode(_ mode: PasscodeSetupControllerMode) {
|
||||
self.mode = mode
|
||||
self.inputFieldNode.reset()
|
||||
|
@ -1039,6 +1039,10 @@ public func channelAdminController(context: AccountContext, peerId: PeerId, admi
|
||||
return current.withUpdatedUpdating(true)
|
||||
}
|
||||
updateRightsDisposable.set((context.peerChannelMemberCategoriesContextsManager.updateMemberAdminRights(account: context.account, peerId: peerId, memberId: adminId, adminRights: TelegramChatAdminRights(rights: updateFlags ?? []), rank: effectiveRank) |> deliverOnMainQueue).start(error: { error in
|
||||
updateState { current in
|
||||
return current.withUpdatedUpdating(false)
|
||||
}
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
var text = presentationData.strings.Login_UnknownError
|
||||
switch error {
|
||||
|
@ -264,14 +264,11 @@ private func completeRights(_ flags: TelegramChatBannedRightsFlags) -> TelegramC
|
||||
result.insert(.banSendStickers)
|
||||
result.insert(.banSendGifs)
|
||||
result.insert(.banSendGames)
|
||||
result.insert(.banSendInline)
|
||||
} else {
|
||||
result.remove(.banSendStickers)
|
||||
result.remove(.banSendGifs)
|
||||
result.remove(.banSendGames)
|
||||
}
|
||||
if result.contains(.banEmbedLinks) {
|
||||
result.insert(.banSendInline)
|
||||
} else {
|
||||
result.remove(.banSendInline)
|
||||
}
|
||||
return result
|
||||
|
@ -239,7 +239,7 @@ private final class ProxyServerInfoItemNode: ActionSheetItemNode {
|
||||
attributedString = NSAttributedString(string: text, font: textFont, textColor: theme.destructiveActionTextColor)
|
||||
}
|
||||
self.statusTextNode.attributedText = attributedString
|
||||
self.setNeedsLayout()
|
||||
self.requestLayoutUpdate()
|
||||
}
|
||||
|
||||
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
@ -396,7 +396,7 @@ private final class ProxyServerActionItemNode: ActionSheetItemNode {
|
||||
strongSelf.buttonNode.isUserInteractionEnabled = false
|
||||
strongSelf.titleNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.SocksProxySetup_Connecting, font: Font.regular(20.0), textColor: strongSelf.theme.primaryTextColor)
|
||||
strongSelf.activityIndicator.isHidden = false
|
||||
strongSelf.setNeedsLayout()
|
||||
strongSelf.requestLayoutUpdate()
|
||||
|
||||
let signal = strongSelf.network.connectionStatus
|
||||
|> filter { status in
|
||||
@ -428,7 +428,7 @@ private final class ProxyServerActionItemNode: ActionSheetItemNode {
|
||||
})
|
||||
strongSelf.titleNode.attributedText = NSAttributedString(string: strongSelf.presentationData.strings.SocksProxySetup_ConnectAndSave, font: Font.regular(20.0), textColor: strongSelf.theme.controlAccentColor)
|
||||
strongSelf.buttonNode.isUserInteractionEnabled = true
|
||||
strongSelf.setNeedsLayout()
|
||||
strongSelf.requestLayoutUpdate()
|
||||
|
||||
strongSelf.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: strongSelf.presentationData), title: nil, text: strongSelf.presentationData.strings.SocksProxySetup_FailedToConnect, actions: [TextAlertAction(type: .defaultAction, title: strongSelf.presentationData.strings.Common_OK, action: {})]), nil)
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ private final class ProxyServerQRCodeItemNode: ActionSheetItemNode {
|
||||
self.addSubnode(self.imageNode)
|
||||
}
|
||||
|
||||
override func calculateSizeThatFits(_ constrainedSize: CGSize) -> CGSize {
|
||||
override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
let imageInset: CGFloat = 44.0
|
||||
let side = constrainedSize.width - imageInset * 2.0
|
||||
var imageSize = CGSize(width: side, height: side)
|
||||
@ -162,18 +162,10 @@ private final class ProxyServerQRCodeItemNode: ActionSheetItemNode {
|
||||
if !self.cachedHasLabel {
|
||||
labelSize = CGSize()
|
||||
}
|
||||
return CGSize(width: constrainedSize.width, height: 14.0 + (labelSize.height > 0.0 ? labelSize.height + 14.0 : 0.0) + (imageSize.height > 0.0 ? imageSize.height + 14.0 : 8.0))
|
||||
}
|
||||
|
||||
override func layout() {
|
||||
super.layout()
|
||||
let size = CGSize(width: constrainedSize.width, height: 14.0 + (labelSize.height > 0.0 ? labelSize.height + 14.0 : 0.0) + (imageSize.height > 0.0 ? imageSize.height + 14.0 : 8.0))
|
||||
|
||||
let size = self.bounds.size
|
||||
let inset: CGFloat = 32.0
|
||||
let imageInset: CGFloat = 44.0
|
||||
let spacing: CGFloat = 18.0
|
||||
|
||||
let labelSize: CGSize
|
||||
if self.cachedHasLabel {
|
||||
labelSize = self.label.measure(CGSize(width: max(1.0, size.width - inset * 2.0), height: size.height))
|
||||
self.label.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - labelSize.width) / 2.0), y: spacing), size: labelSize)
|
||||
@ -181,13 +173,15 @@ private final class ProxyServerQRCodeItemNode: ActionSheetItemNode {
|
||||
labelSize = CGSize()
|
||||
}
|
||||
|
||||
let imageOrigin = CGPoint(x: imageInset, y: self.label.frame.maxY + spacing - 4.0)
|
||||
var imageSize: CGSize
|
||||
if !self.cachedHasImage {
|
||||
imageSize = CGSize()
|
||||
} else {
|
||||
imageSize = CGSize(width: size.width - imageInset * 2.0, height: size.width - imageInset * 2.0)
|
||||
}
|
||||
let imageOrigin = CGPoint(x: imageInset, y: self.label.frame.maxY + spacing - 4.0)
|
||||
self.imageNode.frame = CGRect(origin: imageOrigin, size: imageSize)
|
||||
|
||||
self.updateInternalLayout(size, constrainedSize: constrainedSize)
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
@ -294,6 +294,7 @@ class ItemListRecentSessionItemNode: ItemListRevealOptionsItemNode {
|
||||
}
|
||||
value += string
|
||||
}
|
||||
strongSelf.activateArea.accessibilityValue = value
|
||||
|
||||
if item.enabled {
|
||||
strongSelf.activateArea.accessibilityTraits = []
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -55,9 +55,9 @@ public func stringForMediumDate(timestamp: Int32, strings: PresentationStrings,
|
||||
let separator = dateTimeFormat.dateSeparator
|
||||
switch dateTimeFormat.dateFormat {
|
||||
case .monthFirst:
|
||||
dateString = String(format: "%d%@%d%@%02d", month, separator, day, separator, year - 100)
|
||||
dateString = String(format: "%02d%@%02d%@%02d", month, separator, day, separator, year - 100)
|
||||
case .dayFirst:
|
||||
dateString = String(format: "%d%@%02d%@%02d", day, separator, month, separator, year - 100)
|
||||
dateString = String(format: "%02d%@%02d%@%02d", day, separator, month, separator, year - 100)
|
||||
}
|
||||
|
||||
let timeString = stringForShortTimestamp(hours: Int32(timeinfo.tm_hour), minutes: Int32(timeinfo.tm_min), dateTimeFormat: dateTimeFormat)
|
||||
|
Binary file not shown.
@ -1382,6 +1382,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
}, openInstantPage: { [weak self] message, associatedData in
|
||||
if let strongSelf = self, strongSelf.isNodeLoaded, let navigationController = strongSelf.effectiveNavigationController, let message = strongSelf.chatDisplayNode.historyNode.messageInCurrentHistoryView(message.id) {
|
||||
strongSelf.chatDisplayNode.dismissInput()
|
||||
openChatInstantPage(context: strongSelf.context, message: message, sourcePeerType: associatedData?.automaticDownloadPeerType, navigationController: navigationController)
|
||||
|
||||
if case .overlay = strongSelf.presentationInterfaceState.mode {
|
||||
@ -2646,7 +2647,6 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
chatInfoButtonItem.target = self
|
||||
chatInfoButtonItem.action = #selector(self.rightNavigationButtonAction)
|
||||
chatInfoButtonItem.accessibilityLabel = self.presentationData.strings.Conversation_Info
|
||||
self.chatInfoNavigationButton = ChatNavigationButton(action: .openChatInfo(expandAvatar: true), buttonItem: chatInfoButtonItem)
|
||||
|
||||
self.navigationItem.titleView = self.chatTitleView
|
||||
@ -2801,6 +2801,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
}
|
||||
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.avatarNode.setPeer(context: strongSelf.context, theme: strongSelf.presentationData.theme, peer: peer, overrideImage: imageOverride)
|
||||
(strongSelf.chatInfoNavigationButton?.buttonItem.customDisplayNode as? ChatAvatarNavigationNode)?.contextActionIsEnabled = peer.restrictionText(platform: "ios", contentSettings: strongSelf.context.currentContentSettings.with { $0 }) == nil
|
||||
strongSelf.chatInfoNavigationButton?.buttonItem.accessibilityLabel = presentationInterfaceState.strings.Conversation_ContextMenuOpenProfile
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8465,6 +8466,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
legacyController?.dismiss()
|
||||
}
|
||||
|
||||
legacyController.blocksBackgroundWhenInOverlay = true
|
||||
strongSelf.present(legacyController, in: .window(.root))
|
||||
controller.present(in: emptyController, sourceView: nil, animated: true)
|
||||
|
||||
@ -11641,12 +11643,14 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
|
||||
var latestNode: (Int32, ASDisplayNode)?
|
||||
self.chatDisplayNode.historyNode.forEachVisibleItemNode { itemNode in
|
||||
if let itemNode = itemNode as? ChatMessageItemView, let item = itemNode.item, let statusNode = itemNode.getStatusNode() {
|
||||
if let (latestTimestamp, _) = latestNode {
|
||||
if item.message.timestamp > latestTimestamp {
|
||||
if !item.content.effectivelyIncoming(self.context.account.peerId) {
|
||||
if let (latestTimestamp, _) = latestNode {
|
||||
if item.message.timestamp > latestTimestamp {
|
||||
latestNode = (item.message.timestamp, statusNode)
|
||||
}
|
||||
} else {
|
||||
latestNode = (item.message.timestamp, statusNode)
|
||||
}
|
||||
} else {
|
||||
latestNode = (item.message.timestamp, statusNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView,
|
||||
if id == cachedChannelAdminRanksEntryId(peerId: peerId), let data = data as? CachedChannelAdminRanks {
|
||||
adminRanks = data.ranks
|
||||
}
|
||||
} else if case let .peer(_, peer) = additionalEntry, let channel = peer as? TelegramChannel {
|
||||
} else if case let .peer(_, peer) = additionalEntry, let channel = peer as? TelegramChannel, !channel.flags.contains(.isGigagroup) {
|
||||
if let defaultBannedRights = channel.defaultBannedRights, defaultBannedRights.flags.contains(.banSendStickers) {
|
||||
stickersEnabled = false
|
||||
}
|
||||
|
@ -892,7 +892,7 @@ func contextMenuForChatPresentationInterfaceState(chatPresentationInterfaceState
|
||||
}
|
||||
|
||||
var clearCacheAsDelete = false
|
||||
if message.id.peerId.namespace == Namespaces.Peer.CloudChannel && !isMigrated {
|
||||
if let channel = message.peers[message.id.peerId] as? TelegramChannel, case .broadcast = channel.info, !isMigrated {
|
||||
var views: Int = 0
|
||||
for attribute in message.attributes {
|
||||
if let attribute = attribute as? ViewCountMessageAttribute {
|
||||
|
@ -78,7 +78,9 @@ func rightNavigationButtonForChatInterfaceState(_ presentationInterfaceState: Ch
|
||||
if let currentButton = currentButton, currentButton.action == .cancelMessageSelection {
|
||||
return currentButton
|
||||
} else {
|
||||
return ChatNavigationButton(action: .cancelMessageSelection, buttonItem: UIBarButtonItem(title: strings.Common_Cancel, style: .plain, target: target, action: selector))
|
||||
let buttonItem = UIBarButtonItem(title: strings.Common_Cancel, style: .plain, target: target, action: selector)
|
||||
buttonItem.accessibilityLabel = strings.Common_Cancel
|
||||
return ChatNavigationButton(action: .cancelMessageSelection, buttonItem: buttonItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -176,6 +176,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
|
||||
private var actionButtonsNode: ChatMessageActionButtonsNode?
|
||||
|
||||
private let messageAccessibilityArea: AccessibilityAreaNode
|
||||
|
||||
private var highlightedState: Bool = false
|
||||
|
||||
private var forceStopAnimations = false
|
||||
@ -199,6 +201,8 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
self.placeholderNode = StickerShimmerEffectNode()
|
||||
self.placeholderNode.isUserInteractionEnabled = false
|
||||
|
||||
self.messageAccessibilityArea = AccessibilityAreaNode()
|
||||
|
||||
super.init(layerBacked: false)
|
||||
|
||||
self.containerNode.shouldBegin = { [weak self] location in
|
||||
@ -261,8 +265,12 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
self.addSubnode(self.containerNode)
|
||||
self.contextSourceNode.contentNode.addSubnode(self.imageNode)
|
||||
self.contextSourceNode.contentNode.addSubnode(self.placeholderNode)
|
||||
|
||||
self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode)
|
||||
self.addSubnode(self.messageAccessibilityArea)
|
||||
|
||||
self.messageAccessibilityArea.focused = { [weak self] in
|
||||
self?.accessibilityElementDidBecomeFocused()
|
||||
}
|
||||
|
||||
self.dateAndStatusNode.openReactions = { [weak self] in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
@ -569,6 +577,37 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
}
|
||||
|
||||
override func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) {
|
||||
super.updateAccessibilityData(accessibilityData)
|
||||
|
||||
self.messageAccessibilityArea.accessibilityLabel = accessibilityData.label
|
||||
self.messageAccessibilityArea.accessibilityValue = accessibilityData.value
|
||||
self.messageAccessibilityArea.accessibilityHint = accessibilityData.hint
|
||||
self.messageAccessibilityArea.accessibilityTraits = accessibilityData.traits
|
||||
if let customActions = accessibilityData.customActions {
|
||||
self.messageAccessibilityArea.accessibilityCustomActions = customActions.map({ action -> UIAccessibilityCustomAction in
|
||||
return ChatMessageAccessibilityCustomAction(name: action.name, target: self, selector: #selector(self.performLocalAccessibilityCustomAction(_:)), action: action.action)
|
||||
})
|
||||
} else {
|
||||
self.messageAccessibilityArea.accessibilityCustomActions = nil
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func performLocalAccessibilityCustomAction(_ action: UIAccessibilityCustomAction) {
|
||||
if let action = action as? ChatMessageAccessibilityCustomAction {
|
||||
switch action.action {
|
||||
case .reply:
|
||||
if let item = self.item {
|
||||
item.controllerInteraction.setupReply(item.message.id)
|
||||
}
|
||||
case .options:
|
||||
if let item = self.item {
|
||||
item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.imageNode.frame, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, Bool) -> Void) {
|
||||
let displaySize = CGSize(width: 184.0, height: 184.0)
|
||||
let telegramFile = self.telegramFile
|
||||
@ -590,6 +629,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
let currentForwardInfo = self.appliedForwardInfo
|
||||
|
||||
return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in
|
||||
let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: nil)
|
||||
let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params, presentationData: item.presentationData)
|
||||
let incoming = item.message.effectivelyIncoming(item.context.account.peerId)
|
||||
var imageSize: CGSize = CGSize(width: 200.0, height: 200.0)
|
||||
@ -932,7 +972,9 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||
return (ListViewItemNodeLayout(contentSize: layoutSize, insets: layoutInsets), { [weak self] animation, _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature)
|
||||
strongSelf.updateAccessibilityData(accessibilityData)
|
||||
|
||||
strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
|
@ -488,9 +488,18 @@ class ChatMessageBubbleItemNode: ChatMessageItemView, ChatMessagePreviewItemNode
|
||||
guard let strongSelf = self, let accessibilityData = strongSelf.accessibilityData else {
|
||||
return false
|
||||
}
|
||||
|
||||
for node in strongSelf.contentNodes {
|
||||
if node.accessibilityActivate() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if let singleUrl = accessibilityData.singleUrl {
|
||||
strongSelf.item?.controllerInteraction.openUrl(singleUrl, false, false, strongSelf.item?.content.firstMessage)
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -54,6 +54,11 @@ class ChatMessageCallBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self.buttonNode.addTarget(self, action: #selector(self.callButtonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
override func accessibilityActivate() -> Bool {
|
||||
self.callButtonPressed()
|
||||
return true
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
@ -44,6 +44,11 @@ class ChatMessageContactBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
|
||||
}
|
||||
|
||||
override func accessibilityActivate() -> Bool {
|
||||
self.buttonPressed()
|
||||
return true
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
@ -59,6 +59,13 @@ class ChatMessageFileBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func accessibilityActivate() -> Bool {
|
||||
if let item = self.item {
|
||||
let _ = item.controllerInteraction.openMessage(item.message, .default)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
@ -31,6 +31,13 @@ final class ChatMessageGameBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func accessibilityActivate() -> Bool {
|
||||
if let item = self.item {
|
||||
item.controllerInteraction.requestMessageActionCallback(item.message.id, nil, true, false)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
@ -43,6 +43,8 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
||||
|
||||
private var actionButtonsNode: ChatMessageActionButtonsNode?
|
||||
|
||||
private let messageAccessibilityArea: AccessibilityAreaNode
|
||||
|
||||
private var currentSwipeToReplyTranslation: CGFloat = 0.0
|
||||
|
||||
private var recognizer: TapLongTapOrDoubleTapGestureRecognizer?
|
||||
@ -64,6 +66,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
||||
self.contextSourceNode = ContextExtractedContentContainingNode()
|
||||
self.containerNode = ContextControllerSourceNode()
|
||||
self.interactiveVideoNode = ChatMessageInteractiveInstantVideoNode()
|
||||
self.messageAccessibilityArea = AccessibilityAreaNode()
|
||||
|
||||
super.init(layerBacked: false)
|
||||
|
||||
@ -109,6 +112,19 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
||||
self.containerNode.targetNodeForActivationProgress = self.contextSourceNode.contentNode
|
||||
self.addSubnode(self.containerNode)
|
||||
self.contextSourceNode.contentNode.addSubnode(self.interactiveVideoNode)
|
||||
self.addSubnode(self.messageAccessibilityArea)
|
||||
|
||||
self.messageAccessibilityArea.activate = { [weak self] in
|
||||
guard let strongSelf = self, let accessibilityData = strongSelf.accessibilityData else {
|
||||
return false
|
||||
}
|
||||
|
||||
return strongSelf.interactiveVideoNode.accessibilityActivate()
|
||||
}
|
||||
|
||||
self.messageAccessibilityArea.focused = { [weak self] in
|
||||
self?.accessibilityElementDidBecomeFocused()
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
@ -153,6 +169,37 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
||||
self.view.addGestureRecognizer(replyRecognizer)
|
||||
}
|
||||
|
||||
override func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) {
|
||||
super.updateAccessibilityData(accessibilityData)
|
||||
|
||||
self.messageAccessibilityArea.accessibilityLabel = accessibilityData.label
|
||||
self.messageAccessibilityArea.accessibilityValue = accessibilityData.value
|
||||
self.messageAccessibilityArea.accessibilityHint = accessibilityData.hint
|
||||
self.messageAccessibilityArea.accessibilityTraits = accessibilityData.traits
|
||||
if let customActions = accessibilityData.customActions {
|
||||
self.messageAccessibilityArea.accessibilityCustomActions = customActions.map({ action -> UIAccessibilityCustomAction in
|
||||
return ChatMessageAccessibilityCustomAction(name: action.name, target: self, selector: #selector(self.performLocalAccessibilityCustomAction(_:)), action: action.action)
|
||||
})
|
||||
} else {
|
||||
self.messageAccessibilityArea.accessibilityCustomActions = nil
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func performLocalAccessibilityCustomAction(_ action: UIAccessibilityCustomAction) {
|
||||
if let action = action as? ChatMessageAccessibilityCustomAction {
|
||||
switch action.action {
|
||||
case .reply:
|
||||
if let item = self.item {
|
||||
item.controllerInteraction.setupReply(item.message.id)
|
||||
}
|
||||
case .options:
|
||||
if let item = self.item {
|
||||
item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.interactiveVideoNode.frame, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, Bool) -> Void) {
|
||||
let layoutConstants = self.layoutConstants
|
||||
|
||||
@ -172,6 +219,8 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
||||
let currentForwardInfo = self.appliedForwardInfo
|
||||
|
||||
return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in
|
||||
let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: nil)
|
||||
|
||||
let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params, presentationData: item.presentationData)
|
||||
let incoming = item.message.effectivelyIncoming(item.context.account.peerId)
|
||||
|
||||
@ -449,10 +498,13 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
||||
strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
|
||||
strongSelf.appliedItem = item
|
||||
strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature)
|
||||
|
||||
strongSelf.updateAccessibilityData(accessibilityData)
|
||||
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animation.isAnimated {
|
||||
transition = .animated(duration: 0.2, curve: .spring)
|
||||
|
@ -757,6 +757,11 @@ class ChatMessageInteractiveInstantVideoNode: ASDisplayNode {
|
||||
}
|
||||
}
|
||||
|
||||
override func accessibilityActivate() -> Bool {
|
||||
self.progressPressed()
|
||||
return true
|
||||
}
|
||||
|
||||
func videoContentNode(at point: CGPoint) -> ASDisplayNode? {
|
||||
if let videoFrame = self.videoFrame {
|
||||
if videoFrame.contains(point) {
|
||||
|
@ -144,7 +144,6 @@ private let voiceMessageDurationFormatter: DateComponentsFormatter = {
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.unitsStyle = .spellOut
|
||||
formatter.allowedUnits = [.minute, .second]
|
||||
formatter.zeroFormattingBehavior = .pad
|
||||
return formatter
|
||||
}()
|
||||
|
||||
@ -152,7 +151,6 @@ private let musicDurationFormatter: DateComponentsFormatter = {
|
||||
let formatter = DateComponentsFormatter()
|
||||
formatter.unitsStyle = .spellOut
|
||||
formatter.allowedUnits = [.hour, .minute, .second]
|
||||
formatter.zeroFormattingBehavior = .pad
|
||||
return formatter
|
||||
}()
|
||||
|
||||
@ -186,8 +184,6 @@ final class ChatMessageAccessibilityData {
|
||||
let singleUrl: String?
|
||||
|
||||
init(item: ChatMessageItem, isSelected: Bool?) {
|
||||
var label: String = ""
|
||||
let value: String
|
||||
var hint: String?
|
||||
var traits: UIAccessibilityTraits = []
|
||||
var singleUrl: String?
|
||||
@ -204,344 +200,377 @@ final class ChatMessageAccessibilityData {
|
||||
}
|
||||
}
|
||||
|
||||
let authorName = item.message.author?.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
||||
|
||||
if let chatPeer = item.message.peers[item.message.id.peerId] {
|
||||
let (_, _, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, messages: [item.message], chatPeer: RenderedPeer(peer: chatPeer), accountPeerId: item.context.account.peerId)
|
||||
let dataForMessage: (Message, Bool) -> (String, String) = { message, isReply -> (String, String) in
|
||||
var label: String = ""
|
||||
var value: String = ""
|
||||
|
||||
var text = messageText
|
||||
|
||||
loop: for media in item.message.media {
|
||||
if let _ = media as? TelegramMediaImage {
|
||||
if isIncoming {
|
||||
if announceIncomingAuthors, let authorName = authorName {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_PhotoFrom(authorName).0
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_Photo
|
||||
}
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_YourPhoto
|
||||
}
|
||||
text = ""
|
||||
if !item.message.text.isEmpty {
|
||||
text.append("\n")
|
||||
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_Caption(item.message.text).0)
|
||||
}
|
||||
} else if let file = media as? TelegramMediaFile {
|
||||
var isSpecialFile = false
|
||||
for attribute in file.attributes {
|
||||
switch attribute {
|
||||
case let .Audio(audio):
|
||||
isSpecialFile = true
|
||||
if isSelected == nil {
|
||||
hint = item.presentationData.strings.VoiceOver_Chat_PlayHint
|
||||
}
|
||||
traits.insert(.startsMediaSession)
|
||||
if audio.isVoice {
|
||||
let durationString = voiceMessageDurationFormatter.string(from: Double(audio.duration)) ?? ""
|
||||
if isIncoming {
|
||||
if announceIncomingAuthors, let authorName = authorName {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_VoiceMessageFrom(authorName).0
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_VoiceMessage
|
||||
}
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_YourVoiceMessage
|
||||
}
|
||||
text = item.presentationData.strings.VoiceOver_Chat_Duration(durationString).0
|
||||
} else {
|
||||
let durationString = musicDurationFormatter.string(from: Double(audio.duration)) ?? ""
|
||||
if isIncoming {
|
||||
if announceIncomingAuthors, let authorName = authorName {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_MusicFrom(authorName).0
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_Music
|
||||
}
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_YourMusic
|
||||
}
|
||||
let performer = audio.performer ?? "Unknown"
|
||||
let title = audio.title ?? "Unknown"
|
||||
|
||||
text = item.presentationData.strings.VoiceOver_Chat_MusicTitle(title, performer).0
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_Duration(durationString).0)
|
||||
}
|
||||
case let .Video(video):
|
||||
isSpecialFile = true
|
||||
if isSelected == nil {
|
||||
hint = item.presentationData.strings.VoiceOver_Chat_PlayHint
|
||||
}
|
||||
traits.insert(.startsMediaSession)
|
||||
let durationString = voiceMessageDurationFormatter.string(from: Double(video.duration)) ?? ""
|
||||
if video.flags.contains(.instantRoundVideo) {
|
||||
if isIncoming {
|
||||
if announceIncomingAuthors, let authorName = authorName {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_VideoMessageFrom(authorName).0
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_VideoMessage
|
||||
}
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_YourVideoMessage
|
||||
}
|
||||
} else {
|
||||
if isIncoming {
|
||||
if announceIncomingAuthors, let authorName = authorName {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_VideoFrom(authorName).0
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_Video
|
||||
}
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_YourVideo
|
||||
}
|
||||
}
|
||||
text = item.presentationData.strings.VoiceOver_Chat_Duration(durationString).0
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isSpecialFile {
|
||||
if isSelected == nil {
|
||||
hint = item.presentationData.strings.VoiceOver_Chat_OpenHint
|
||||
}
|
||||
let sizeString = fileSizeFormatter.string(fromByteCount: Int64(file.size ?? 0))
|
||||
if let chatPeer = message.peers[item.message.id.peerId] {
|
||||
let authorName = message.author?.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
||||
|
||||
let (_, _, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, messages: [message], chatPeer: RenderedPeer(peer: chatPeer), accountPeerId: item.context.account.peerId)
|
||||
|
||||
var text = messageText
|
||||
|
||||
loop: for media in message.media {
|
||||
if let _ = media as? TelegramMediaImage {
|
||||
if isIncoming {
|
||||
if announceIncomingAuthors, let authorName = authorName {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_FileFrom(authorName).0
|
||||
label = item.presentationData.strings.VoiceOver_Chat_PhotoFrom(authorName).0
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_File
|
||||
label = item.presentationData.strings.VoiceOver_Chat_Photo
|
||||
}
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_YourFile
|
||||
label = item.presentationData.strings.VoiceOver_Chat_YourPhoto
|
||||
}
|
||||
text = "\(file.fileName ?? ""). "
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_Size(sizeString).0)
|
||||
}
|
||||
if !item.message.text.isEmpty {
|
||||
text.append("\n")
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_Caption(item.message.text).0)
|
||||
}
|
||||
break loop
|
||||
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
|
||||
var contentText = item.presentationData.strings.VoiceOver_Chat_PagePreview + ". "
|
||||
if let title = content.title, !title.isEmpty {
|
||||
contentText.append(item.presentationData.strings.VoiceOver_Chat_Title(title).0)
|
||||
contentText.append(". ")
|
||||
}
|
||||
if let text = content.text, !text.isEmpty {
|
||||
contentText.append(text)
|
||||
}
|
||||
text = "\(item.message.text)\n\(contentText)"
|
||||
} else if let contact = media as? TelegramMediaContact {
|
||||
if isIncoming {
|
||||
if announceIncomingAuthors, let authorName = authorName {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_ContactFrom(authorName).0
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_Contact
|
||||
text = ""
|
||||
if !message.text.isEmpty {
|
||||
text.append("\n")
|
||||
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_Caption(message.text).0)
|
||||
}
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_YourContact
|
||||
}
|
||||
var displayName = ""
|
||||
if !contact.firstName.isEmpty {
|
||||
displayName.append(contact.firstName)
|
||||
}
|
||||
if !contact.lastName.isEmpty {
|
||||
if !displayName.isEmpty {
|
||||
displayName.append(" ")
|
||||
} else if let file = media as? TelegramMediaFile {
|
||||
var isSpecialFile = false
|
||||
for attribute in file.attributes {
|
||||
switch attribute {
|
||||
case let .Sticker(displayText, packReference, _):
|
||||
isSpecialFile = true
|
||||
text = displayText
|
||||
if file.mimeType == "application/x-tgsticker" {
|
||||
if isIncoming {
|
||||
if announceIncomingAuthors, let authorName = authorName {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_AnimatedStickerFrom(authorName).0
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_AnimatedSticker
|
||||
}
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_YourAnimatedSticker
|
||||
}
|
||||
} else {
|
||||
if isIncoming {
|
||||
if announceIncomingAuthors, let authorName = authorName {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_StickerFrom(authorName).0
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_Sticker
|
||||
}
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_YourSticker
|
||||
}
|
||||
}
|
||||
case let .Audio(audio):
|
||||
isSpecialFile = true
|
||||
if isSelected == nil {
|
||||
hint = item.presentationData.strings.VoiceOver_Chat_PlayHint
|
||||
}
|
||||
traits.insert(.startsMediaSession)
|
||||
if audio.isVoice {
|
||||
let durationString = voiceMessageDurationFormatter.string(from: Double(audio.duration)) ?? ""
|
||||
if isIncoming {
|
||||
if announceIncomingAuthors, let authorName = authorName {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_VoiceMessageFrom(authorName).0
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_VoiceMessage
|
||||
}
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_YourVoiceMessage
|
||||
}
|
||||
text = item.presentationData.strings.VoiceOver_Chat_Duration(durationString).0
|
||||
} else {
|
||||
let durationString = musicDurationFormatter.string(from: Double(audio.duration)) ?? ""
|
||||
if isIncoming {
|
||||
if announceIncomingAuthors, let authorName = authorName {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_MusicFrom(authorName).0
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_Music
|
||||
}
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_YourMusic
|
||||
}
|
||||
let performer = audio.performer ?? "Unknown"
|
||||
let title = audio.title ?? "Unknown"
|
||||
|
||||
text = item.presentationData.strings.VoiceOver_Chat_MusicTitle(title, performer).0
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_Duration(durationString).0)
|
||||
}
|
||||
case let .Video(video):
|
||||
isSpecialFile = true
|
||||
if isSelected == nil {
|
||||
hint = item.presentationData.strings.VoiceOver_Chat_PlayHint
|
||||
}
|
||||
traits.insert(.startsMediaSession)
|
||||
let durationString = voiceMessageDurationFormatter.string(from: Double(video.duration)) ?? ""
|
||||
if video.flags.contains(.instantRoundVideo) {
|
||||
if isIncoming {
|
||||
if announceIncomingAuthors, let authorName = authorName {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_VideoMessageFrom(authorName).0
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_VideoMessage
|
||||
}
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_YourVideoMessage
|
||||
}
|
||||
} else {
|
||||
if isIncoming {
|
||||
if announceIncomingAuthors, let authorName = authorName {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_VideoFrom(authorName).0
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_Video
|
||||
}
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_YourVideo
|
||||
}
|
||||
}
|
||||
text = item.presentationData.strings.VoiceOver_Chat_Duration(durationString).0
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
displayName.append(contact.lastName)
|
||||
}
|
||||
var phoneNumbersString = ""
|
||||
var phoneNumberCount = 0
|
||||
var emailAddressesString = ""
|
||||
var emailAddressCount = 0
|
||||
var organizationString = ""
|
||||
if let vCard = contact.vCardData, let vCardData = vCard.data(using: .utf8), let contactData = DeviceContactExtendedData(vcard: vCardData) {
|
||||
if displayName.isEmpty && !contactData.organization.isEmpty {
|
||||
displayName = contactData.organization
|
||||
}
|
||||
if !contactData.basicData.phoneNumbers.isEmpty {
|
||||
for phone in contactData.basicData.phoneNumbers {
|
||||
if !phoneNumbersString.isEmpty {
|
||||
phoneNumbersString.append(", ")
|
||||
if !isSpecialFile {
|
||||
if isSelected == nil {
|
||||
hint = item.presentationData.strings.VoiceOver_Chat_OpenHint
|
||||
}
|
||||
let sizeString = fileSizeFormatter.string(fromByteCount: Int64(file.size ?? 0))
|
||||
if isIncoming {
|
||||
if announceIncomingAuthors, let authorName = authorName {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_FileFrom(authorName).0
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_File
|
||||
}
|
||||
for c in phone.value {
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_YourFile
|
||||
}
|
||||
text = "\(file.fileName ?? ""). "
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_Size(sizeString).0)
|
||||
}
|
||||
if !message.text.isEmpty {
|
||||
text.append("\n")
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_Caption(message.text).0)
|
||||
}
|
||||
break loop
|
||||
} else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content {
|
||||
var contentText = item.presentationData.strings.VoiceOver_Chat_PagePreview + ". "
|
||||
if let title = content.title, !title.isEmpty {
|
||||
contentText.append(item.presentationData.strings.VoiceOver_Chat_Title(title).0)
|
||||
contentText.append(". ")
|
||||
}
|
||||
if let text = content.text, !text.isEmpty {
|
||||
contentText.append(text)
|
||||
}
|
||||
text = "\(message.text)\n\(contentText)"
|
||||
} else if let contact = media as? TelegramMediaContact {
|
||||
if isIncoming {
|
||||
if announceIncomingAuthors, let authorName = authorName {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_ContactFrom(authorName).0
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_Contact
|
||||
}
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_YourContact
|
||||
}
|
||||
var displayName = ""
|
||||
if !contact.firstName.isEmpty {
|
||||
displayName.append(contact.firstName)
|
||||
}
|
||||
if !contact.lastName.isEmpty {
|
||||
if !displayName.isEmpty {
|
||||
displayName.append(" ")
|
||||
}
|
||||
displayName.append(contact.lastName)
|
||||
}
|
||||
var phoneNumbersString = ""
|
||||
var phoneNumberCount = 0
|
||||
var emailAddressesString = ""
|
||||
var emailAddressCount = 0
|
||||
var organizationString = ""
|
||||
if let vCard = contact.vCardData, let vCardData = vCard.data(using: .utf8), let contactData = DeviceContactExtendedData(vcard: vCardData) {
|
||||
if displayName.isEmpty && !contactData.organization.isEmpty {
|
||||
displayName = contactData.organization
|
||||
}
|
||||
if !contactData.basicData.phoneNumbers.isEmpty {
|
||||
for phone in contactData.basicData.phoneNumbers {
|
||||
if !phoneNumbersString.isEmpty {
|
||||
phoneNumbersString.append(", ")
|
||||
}
|
||||
for c in phone.value {
|
||||
phoneNumbersString.append(c)
|
||||
phoneNumbersString.append(" ")
|
||||
}
|
||||
phoneNumberCount += 1
|
||||
}
|
||||
} else {
|
||||
for c in contact.phoneNumber {
|
||||
phoneNumbersString.append(c)
|
||||
phoneNumbersString.append(" ")
|
||||
}
|
||||
phoneNumberCount += 1
|
||||
}
|
||||
} else {
|
||||
for c in contact.phoneNumber {
|
||||
phoneNumbersString.append(c)
|
||||
phoneNumbersString.append(" ")
|
||||
|
||||
for email in contactData.emailAddresses {
|
||||
if !emailAddressesString.isEmpty {
|
||||
emailAddressesString.append(", ")
|
||||
}
|
||||
emailAddressesString.append("\(email.value)")
|
||||
emailAddressCount += 1
|
||||
}
|
||||
phoneNumberCount += 1
|
||||
if !contactData.organization.isEmpty && displayName != contactData.organization {
|
||||
organizationString = contactData.organization
|
||||
}
|
||||
} else {
|
||||
phoneNumbersString.append("\(contact.phoneNumber)")
|
||||
}
|
||||
text = "\(displayName)."
|
||||
if !phoneNumbersString.isEmpty {
|
||||
if phoneNumberCount > 1 {
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_ContactPhoneNumberCount(Int32(phoneNumberCount)))
|
||||
text.append(": ")
|
||||
} else {
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_ContactPhoneNumber)
|
||||
}
|
||||
text.append("\(phoneNumbersString). ")
|
||||
}
|
||||
if !emailAddressesString.isEmpty {
|
||||
if emailAddressCount > 1 {
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_ContactEmailCount(Int32(emailAddressCount)))
|
||||
text.append(": ")
|
||||
} else {
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_ContactEmail)
|
||||
text.append(": ")
|
||||
}
|
||||
text.append("\(emailAddressesString). ")
|
||||
}
|
||||
if !organizationString.isEmpty {
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_ContactOrganization(organizationString).0)
|
||||
text.append(".")
|
||||
}
|
||||
} else if let poll = media as? TelegramMediaPoll {
|
||||
if isIncoming {
|
||||
if announceIncomingAuthors, let authorName = authorName {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_AnonymousPollFrom(authorName).0
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_AnonymousPoll
|
||||
}
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_YourAnonymousPoll
|
||||
}
|
||||
|
||||
for email in contactData.emailAddresses {
|
||||
if !emailAddressesString.isEmpty {
|
||||
emailAddressesString.append(", ")
|
||||
var optionVoterCount: [Int: Int32] = [:]
|
||||
var maxOptionVoterCount: Int32 = 0
|
||||
var totalVoterCount: Int32 = 0
|
||||
let voters: [TelegramMediaPollOptionVoters]?
|
||||
if poll.isClosed {
|
||||
voters = poll.results.voters ?? []
|
||||
} else {
|
||||
voters = poll.results.voters
|
||||
}
|
||||
var selectedOptionId: Data?
|
||||
if let voters = voters, let totalVoters = poll.results.totalVoters {
|
||||
var didVote = false
|
||||
for voter in voters {
|
||||
if voter.selected {
|
||||
didVote = true
|
||||
selectedOptionId = voter.opaqueIdentifier
|
||||
}
|
||||
}
|
||||
emailAddressesString.append("\(email.value)")
|
||||
emailAddressCount += 1
|
||||
}
|
||||
if !contactData.organization.isEmpty && displayName != contactData.organization {
|
||||
organizationString = contactData.organization
|
||||
}
|
||||
} else {
|
||||
phoneNumbersString.append("\(contact.phoneNumber)")
|
||||
}
|
||||
text = "\(displayName)."
|
||||
if !phoneNumbersString.isEmpty {
|
||||
if phoneNumberCount > 1 {
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_ContactPhoneNumberCount(Int32(phoneNumberCount)))
|
||||
text.append(": ")
|
||||
} else {
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_ContactPhoneNumber)
|
||||
}
|
||||
text.append("\(phoneNumbersString). ")
|
||||
}
|
||||
if !emailAddressesString.isEmpty {
|
||||
if emailAddressCount > 1 {
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_ContactEmailCount(Int32(emailAddressCount)))
|
||||
text.append(": ")
|
||||
} else {
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_ContactEmail)
|
||||
text.append(": ")
|
||||
}
|
||||
text.append("\(emailAddressesString). ")
|
||||
}
|
||||
if !organizationString.isEmpty {
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_ContactOrganization(organizationString).0)
|
||||
text.append(".")
|
||||
}
|
||||
} else if let poll = media as? TelegramMediaPoll {
|
||||
if isIncoming {
|
||||
if announceIncomingAuthors, let authorName = authorName {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_AnonymousPollFrom(authorName).0
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_AnonymousPoll
|
||||
}
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_YourAnonymousPoll
|
||||
}
|
||||
|
||||
var optionVoterCount: [Int: Int32] = [:]
|
||||
var maxOptionVoterCount: Int32 = 0
|
||||
var totalVoterCount: Int32 = 0
|
||||
let voters: [TelegramMediaPollOptionVoters]?
|
||||
if poll.isClosed {
|
||||
voters = poll.results.voters ?? []
|
||||
} else {
|
||||
voters = poll.results.voters
|
||||
}
|
||||
var selectedOptionId: Data?
|
||||
if let voters = voters, let totalVoters = poll.results.totalVoters {
|
||||
var didVote = false
|
||||
for voter in voters {
|
||||
if voter.selected {
|
||||
didVote = true
|
||||
selectedOptionId = voter.opaqueIdentifier
|
||||
}
|
||||
}
|
||||
totalVoterCount = totalVoters
|
||||
if didVote || poll.isClosed {
|
||||
for i in 0 ..< poll.options.count {
|
||||
inner: for optionVoters in voters {
|
||||
if optionVoters.opaqueIdentifier == poll.options[i].opaqueIdentifier {
|
||||
optionVoterCount[i] = optionVoters.count
|
||||
maxOptionVoterCount = max(maxOptionVoterCount, optionVoters.count)
|
||||
break inner
|
||||
totalVoterCount = totalVoters
|
||||
if didVote || poll.isClosed {
|
||||
for i in 0 ..< poll.options.count {
|
||||
inner: for optionVoters in voters {
|
||||
if optionVoters.opaqueIdentifier == poll.options[i].opaqueIdentifier {
|
||||
optionVoterCount[i] = optionVoters.count
|
||||
maxOptionVoterCount = max(maxOptionVoterCount, optionVoters.count)
|
||||
break inner
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var optionVoterCounts: [Int]
|
||||
if totalVoterCount != 0 {
|
||||
optionVoterCounts = countNicePercent(votes: (0 ..< poll.options.count).map({ Int(optionVoterCount[$0] ?? 0) }), total: Int(totalVoterCount))
|
||||
} else {
|
||||
optionVoterCounts = Array(repeating: 0, count: poll.options.count)
|
||||
}
|
||||
|
||||
text = item.presentationData.strings.VoiceOver_Chat_Title(poll.text).0
|
||||
text.append(". ")
|
||||
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_PollOptionCount(Int32(poll.options.count)))
|
||||
text.append(": ")
|
||||
var optionsText = ""
|
||||
for i in 0 ..< poll.options.count {
|
||||
let option = poll.options[i]
|
||||
|
||||
if !optionsText.isEmpty {
|
||||
optionsText.append(", ")
|
||||
}
|
||||
optionsText.append(option.text)
|
||||
if let selectedOptionId = selectedOptionId, selectedOptionId == option.opaqueIdentifier {
|
||||
optionsText.append(", ")
|
||||
optionsText.append(item.presentationData.strings.VoiceOver_Chat_OptionSelected)
|
||||
var optionVoterCounts: [Int]
|
||||
if totalVoterCount != 0 {
|
||||
optionVoterCounts = countNicePercent(votes: (0 ..< poll.options.count).map({ Int(optionVoterCount[$0] ?? 0) }), total: Int(totalVoterCount))
|
||||
} else {
|
||||
optionVoterCounts = Array(repeating: 0, count: poll.options.count)
|
||||
}
|
||||
|
||||
if let _ = optionVoterCount[i] {
|
||||
if maxOptionVoterCount != 0 && totalVoterCount != 0 {
|
||||
optionsText.append(", \(optionVoterCounts[i])%")
|
||||
text = item.presentationData.strings.VoiceOver_Chat_Title(poll.text).0
|
||||
text.append(". ")
|
||||
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_PollOptionCount(Int32(poll.options.count)))
|
||||
text.append(": ")
|
||||
var optionsText = ""
|
||||
for i in 0 ..< poll.options.count {
|
||||
let option = poll.options[i]
|
||||
|
||||
if !optionsText.isEmpty {
|
||||
optionsText.append(", ")
|
||||
}
|
||||
optionsText.append(option.text)
|
||||
if let selectedOptionId = selectedOptionId, selectedOptionId == option.opaqueIdentifier {
|
||||
optionsText.append(", ")
|
||||
optionsText.append(item.presentationData.strings.VoiceOver_Chat_OptionSelected)
|
||||
}
|
||||
|
||||
if let _ = optionVoterCount[i] {
|
||||
if maxOptionVoterCount != 0 && totalVoterCount != 0 {
|
||||
optionsText.append(", \(optionVoterCounts[i])%")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
text.append("\(optionsText). ")
|
||||
if totalVoterCount != 0 {
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_PollVotes(Int32(totalVoterCount)))
|
||||
} else {
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_PollNoVotes)
|
||||
}
|
||||
if poll.isClosed {
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_PollFinalResults)
|
||||
text.append("\(optionsText). ")
|
||||
if totalVoterCount != 0 {
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_PollVotes(Int32(totalVoterCount)))
|
||||
} else {
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_PollNoVotes)
|
||||
}
|
||||
if poll.isClosed {
|
||||
text.append(item.presentationData.strings.VoiceOver_Chat_PollFinalResults)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var result = ""
|
||||
|
||||
if let isSelected = isSelected {
|
||||
if isSelected {
|
||||
result += item.presentationData.strings.VoiceOver_Chat_Selected
|
||||
|
||||
var result = ""
|
||||
|
||||
if let isSelected = isSelected {
|
||||
if isSelected {
|
||||
result += item.presentationData.strings.VoiceOver_Chat_Selected
|
||||
result += "\n"
|
||||
}
|
||||
traits.insert(.startsMediaSession)
|
||||
}
|
||||
|
||||
result += "\(text)"
|
||||
|
||||
let dateString = DateFormatter.localizedString(from: Date(timeIntervalSince1970: Double(message.timestamp)), dateStyle: .medium, timeStyle: .short)
|
||||
|
||||
result += "\n\(dateString)"
|
||||
if !isIncoming && item.read && !isReply {
|
||||
result += "\n"
|
||||
if announceIncomingAuthors {
|
||||
result += item.presentationData.strings.VoiceOver_Chat_SeenByRecipients
|
||||
} else {
|
||||
result += item.presentationData.strings.VoiceOver_Chat_SeenByRecipient
|
||||
}
|
||||
}
|
||||
traits.insert(.startsMediaSession)
|
||||
value = result
|
||||
} else {
|
||||
value = ""
|
||||
}
|
||||
|
||||
result += "\(text)"
|
||||
|
||||
let dateString = DateFormatter.localizedString(from: Date(timeIntervalSince1970: Double(item.message.timestamp)), dateStyle: .medium, timeStyle: .short)
|
||||
|
||||
result += "\n\(dateString)"
|
||||
if !isIncoming && item.read {
|
||||
result += "\n"
|
||||
if announceIncomingAuthors {
|
||||
result += item.presentationData.strings.VoiceOver_Chat_SeenByRecipients
|
||||
if label.isEmpty {
|
||||
if let author = message.author {
|
||||
if isIncoming {
|
||||
label = author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_YourMessage
|
||||
}
|
||||
} else {
|
||||
result += item.presentationData.strings.VoiceOver_Chat_SeenByRecipient
|
||||
label = item.presentationData.strings.VoiceOver_Chat_Message
|
||||
}
|
||||
}
|
||||
value = result
|
||||
} else {
|
||||
value = ""
|
||||
|
||||
return (label, value)
|
||||
}
|
||||
|
||||
if label.isEmpty {
|
||||
if let author = item.message.author {
|
||||
if isIncoming {
|
||||
label = author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_YourMessage
|
||||
}
|
||||
} else {
|
||||
label = item.presentationData.strings.VoiceOver_Chat_Message
|
||||
}
|
||||
}
|
||||
var (label, value) = dataForMessage(item.message, false)
|
||||
|
||||
for attribute in item.message.attributes {
|
||||
if let attribute = attribute as? TextEntitiesMessageAttribute {
|
||||
@ -571,7 +600,7 @@ final class ChatMessageAccessibilityData {
|
||||
}
|
||||
}
|
||||
} else if let attribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[attribute.messageId] {
|
||||
let replyLabel: String
|
||||
var replyLabel: String
|
||||
if replyMessage.flags.contains(.Incoming) {
|
||||
if let author = replyMessage.author {
|
||||
replyLabel = item.presentationData.strings.VoiceOver_Chat_ReplyFrom(author.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)).0
|
||||
@ -581,6 +610,10 @@ final class ChatMessageAccessibilityData {
|
||||
} else {
|
||||
replyLabel = item.presentationData.strings.VoiceOver_Chat_ReplyToYourMessage
|
||||
}
|
||||
|
||||
let (replyMessageLabel, replyMessageValue) = dataForMessage(replyMessage, true)
|
||||
replyLabel += "\(replyLabel): \(replyMessageLabel), \(replyMessageValue)"
|
||||
|
||||
label = "\(replyLabel) . \(label)"
|
||||
}
|
||||
}
|
||||
|
@ -43,6 +43,13 @@ class ChatMessageMapBubbleContentNode: ChatMessageBubbleContentNode {
|
||||
self.addSubnode(self.pinNode)
|
||||
}
|
||||
|
||||
override func accessibilityActivate() -> Bool {
|
||||
if let item = self.item {
|
||||
let _ = item.controllerInteraction.openMessage(item.message, .default)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
@ -42,6 +42,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
|
||||
private var actionButtonsNode: ChatMessageActionButtonsNode?
|
||||
|
||||
private let messageAccessibilityArea: AccessibilityAreaNode
|
||||
|
||||
private var highlightedState: Bool = false
|
||||
|
||||
private var currentSwipeToReplyTranslation: CGFloat = 0.0
|
||||
@ -55,6 +57,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
self.placeholderNode = StickerShimmerEffectNode()
|
||||
self.placeholderNode.isUserInteractionEnabled = false
|
||||
self.dateAndStatusNode = ChatMessageDateAndStatusNode()
|
||||
self.messageAccessibilityArea = AccessibilityAreaNode()
|
||||
|
||||
super.init(layerBacked: false)
|
||||
|
||||
@ -120,6 +123,11 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
self.contextSourceNode.contentNode.addSubnode(self.placeholderNode)
|
||||
self.contextSourceNode.contentNode.addSubnode(self.imageNode)
|
||||
self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode)
|
||||
self.addSubnode(self.messageAccessibilityArea)
|
||||
|
||||
self.messageAccessibilityArea.focused = { [weak self] in
|
||||
self?.accessibilityElementDidBecomeFocused()
|
||||
}
|
||||
|
||||
self.dateAndStatusNode.openReactions = { [weak self] in
|
||||
guard let strongSelf = self, let item = strongSelf.item else {
|
||||
@ -238,6 +246,37 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
}
|
||||
}
|
||||
|
||||
override func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) {
|
||||
super.updateAccessibilityData(accessibilityData)
|
||||
|
||||
self.messageAccessibilityArea.accessibilityLabel = accessibilityData.label
|
||||
self.messageAccessibilityArea.accessibilityValue = accessibilityData.value
|
||||
self.messageAccessibilityArea.accessibilityHint = accessibilityData.hint
|
||||
self.messageAccessibilityArea.accessibilityTraits = accessibilityData.traits
|
||||
if let customActions = accessibilityData.customActions {
|
||||
self.messageAccessibilityArea.accessibilityCustomActions = customActions.map({ action -> UIAccessibilityCustomAction in
|
||||
return ChatMessageAccessibilityCustomAction(name: action.name, target: self, selector: #selector(self.performLocalAccessibilityCustomAction(_:)), action: action.action)
|
||||
})
|
||||
} else {
|
||||
self.messageAccessibilityArea.accessibilityCustomActions = nil
|
||||
}
|
||||
}
|
||||
|
||||
@objc private func performLocalAccessibilityCustomAction(_ action: UIAccessibilityCustomAction) {
|
||||
if let action = action as? ChatMessageAccessibilityCustomAction {
|
||||
switch action.action {
|
||||
case .reply:
|
||||
if let item = self.item {
|
||||
item.controllerInteraction.setupReply(item.message.id)
|
||||
}
|
||||
case .options:
|
||||
if let item = self.item {
|
||||
item.controllerInteraction.openMessageContextMenu(item.message, false, self, self.imageNode.frame, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func asyncLayout() -> (_ item: ChatMessageItem, _ params: ListViewItemLayoutParams, _ mergedTop: ChatMessageMerge, _ mergedBottom: ChatMessageMerge, _ dateHeaderAtBottom: Bool) -> (ListViewItemNodeLayout, (ListViewItemUpdateAnimation, Bool) -> Void) {
|
||||
let displaySize = CGSize(width: 184.0, height: 184.0)
|
||||
let telegramFile = self.telegramFile
|
||||
@ -254,6 +293,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
let currentItem = self.item
|
||||
|
||||
return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in
|
||||
let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: nil)
|
||||
|
||||
let layoutConstants = chatMessageItemLayoutConstants(layoutConstants, params: params, presentationData: item.presentationData)
|
||||
let incoming = item.message.effectivelyIncoming(item.context.account.peerId)
|
||||
var imageSize: CGSize = CGSize(width: 100.0, height: 100.0)
|
||||
@ -598,6 +639,8 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
transition = .animated(duration: duration, curve: .spring)
|
||||
}
|
||||
|
||||
strongSelf.updateAccessibilityData(accessibilityData)
|
||||
|
||||
transition.updateFrame(node: strongSelf.imageNode, frame: updatedImageFrame)
|
||||
imageApply()
|
||||
|
||||
@ -610,6 +653,7 @@ class ChatMessageStickerItemNode: ChatMessageItemView {
|
||||
strongSelf.placeholderNode.frame = placeholderFrame
|
||||
}
|
||||
|
||||
strongSelf.messageAccessibilityArea.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.contextSourceNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
strongSelf.contextSourceNode.contentNode.frame = CGRect(origin: CGPoint(), size: layoutSize)
|
||||
|
@ -135,8 +135,8 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode {
|
||||
}
|
||||
|
||||
func updateAccessibility() {
|
||||
self.accessibilityTraits = .button
|
||||
if !self.micButton.alpha.isZero {
|
||||
self.accessibilityTraits = .button
|
||||
switch self.micButton.mode {
|
||||
case .audio:
|
||||
self.accessibilityLabel = self.strings.VoiceOver_Chat_RecordModeVoiceMessage
|
||||
@ -146,7 +146,6 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode {
|
||||
self.accessibilityHint = self.strings.VoiceOver_Chat_RecordModeVideoMessageInfo
|
||||
}
|
||||
} else {
|
||||
self.accessibilityTraits = .button
|
||||
self.accessibilityLabel = self.strings.MediaPicker_Send
|
||||
self.accessibilityHint = nil
|
||||
}
|
||||
|
@ -19,37 +19,37 @@ import Speak
|
||||
private let accessoryButtonFont = Font.medium(14.0)
|
||||
private let counterFont = Font.with(size: 14.0, design: .regular, traits: [.monospacedNumbers])
|
||||
|
||||
private final class AccessoryItemIconButton: HighlightTrackingButton {
|
||||
private final class AccessoryItemIconButtonNode: HighlightTrackingButtonNode {
|
||||
private let item: ChatTextInputAccessoryItem
|
||||
private var width: CGFloat
|
||||
private let imageNode: ASImageNode
|
||||
private var imageEdgeInsets = UIEdgeInsets()
|
||||
|
||||
init(item: ChatTextInputAccessoryItem, theme: PresentationTheme, strings: PresentationStrings) {
|
||||
self.imageNode = ASImageNode()
|
||||
self.imageNode.isLayerBacked = true
|
||||
self.imageNode.displaysAsynchronously = false
|
||||
self.imageNode.displayWithoutProcessing = true
|
||||
|
||||
self.item = item
|
||||
|
||||
let (image, text, alpha, insets) = AccessoryItemIconButton.imageAndInsets(item: item, theme: theme, strings: strings)
|
||||
let (image, text, accessibilityLabel, alpha, insets) = AccessoryItemIconButtonNode.imageAndInsets(item: item, theme: theme, strings: strings)
|
||||
|
||||
self.width = AccessoryItemIconButton.calculateWidth(item: item, image: image, text: text, strings: strings)
|
||||
self.width = AccessoryItemIconButtonNode.calculateWidth(item: item, image: image, text: text, strings: strings)
|
||||
|
||||
super.init(frame: CGRect())
|
||||
super.init()
|
||||
|
||||
self.isAccessibilityElement = true
|
||||
self.accessibilityTraits = [.button]
|
||||
|
||||
self.addSubnode(self.imageNode)
|
||||
|
||||
if let text = text {
|
||||
self.titleLabel?.font = accessoryButtonFont
|
||||
self.setTitleColor(theme.chat.inputPanel.inputControlColor, for: [])
|
||||
self.setTitle(text, for: [])
|
||||
self.setAttributedTitle(NSAttributedString(string: text, font: accessoryButtonFont, textColor: theme.chat.inputPanel.inputControlColor), for: .normal)
|
||||
} else {
|
||||
self.setAttributedTitle(NSAttributedString(), for: .normal)
|
||||
}
|
||||
|
||||
self.imageNode.image = image
|
||||
self.imageNode.alpha = alpha
|
||||
self.imageEdgeInsets = insets
|
||||
|
||||
self.accessibilityLabel = accessibilityLabel
|
||||
|
||||
self.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
@ -64,51 +64,51 @@ private final class AccessoryItemIconButton: HighlightTrackingButton {
|
||||
}
|
||||
|
||||
func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||
let (image, text, alpha, insets) = AccessoryItemIconButton.imageAndInsets(item: item, theme: theme, strings: strings)
|
||||
let (image, text, accessibilityLabel, alpha, insets) = AccessoryItemIconButtonNode.imageAndInsets(item: item, theme: theme, strings: strings)
|
||||
|
||||
self.width = AccessoryItemIconButton.calculateWidth(item: item, image: image, text: text, strings: strings)
|
||||
self.width = AccessoryItemIconButtonNode.calculateWidth(item: item, image: image, text: text, strings: strings)
|
||||
|
||||
if let text = text {
|
||||
self.titleLabel?.font = accessoryButtonFont
|
||||
self.setTitleColor(theme.chat.inputPanel.inputControlColor, for: [])
|
||||
self.setTitle(text, for: [])
|
||||
self.setAttributedTitle(NSAttributedString(string: text, font: accessoryButtonFont, textColor: theme.chat.inputPanel.inputControlColor), for: .normal)
|
||||
} else {
|
||||
self.setTitle("", for: [])
|
||||
self.setAttributedTitle(NSAttributedString(), for: .normal)
|
||||
}
|
||||
|
||||
self.imageNode.image = image
|
||||
self.imageEdgeInsets = insets
|
||||
self.imageNode.alpha = alpha
|
||||
|
||||
self.accessibilityLabel = accessibilityLabel
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
static func imageAndInsets(item: ChatTextInputAccessoryItem, theme: PresentationTheme, strings: PresentationStrings) -> (UIImage?, String?, CGFloat, UIEdgeInsets) {
|
||||
static func imageAndInsets(item: ChatTextInputAccessoryItem, theme: PresentationTheme, strings: PresentationStrings) -> (UIImage?, String?, String, CGFloat, UIEdgeInsets) {
|
||||
switch item {
|
||||
case .keyboard:
|
||||
return (PresentationResourcesChat.chatInputTextFieldKeyboardImage(theme), nil, 1.0, UIEdgeInsets())
|
||||
return (PresentationResourcesChat.chatInputTextFieldKeyboardImage(theme), nil, strings.VoiceOver_Keyboard, 1.0, UIEdgeInsets())
|
||||
case let .stickers(enabled):
|
||||
return (PresentationResourcesChat.chatInputTextFieldStickersImage(theme), nil, enabled ? 1.0 : 0.4, UIEdgeInsets())
|
||||
return (PresentationResourcesChat.chatInputTextFieldStickersImage(theme), nil, strings.VoiceOver_Stickers, enabled ? 1.0 : 0.4, UIEdgeInsets())
|
||||
case .inputButtons:
|
||||
return (PresentationResourcesChat.chatInputTextFieldInputButtonsImage(theme), nil, 1.0, UIEdgeInsets())
|
||||
return (PresentationResourcesChat.chatInputTextFieldInputButtonsImage(theme), nil, strings.VoiceOver_BotKeyboard, 1.0, UIEdgeInsets())
|
||||
case .commands:
|
||||
return (PresentationResourcesChat.chatInputTextFieldCommandsImage(theme), nil, 1.0, UIEdgeInsets())
|
||||
return (PresentationResourcesChat.chatInputTextFieldCommandsImage(theme), nil, strings.VoiceOver_BotCommands, 1.0, UIEdgeInsets())
|
||||
case let .silentPost(value):
|
||||
if value {
|
||||
return (PresentationResourcesChat.chatInputTextFieldSilentPostOnImage(theme), nil, 1.0, UIEdgeInsets())
|
||||
return (PresentationResourcesChat.chatInputTextFieldSilentPostOnImage(theme), nil, strings.VoiceOver_SilentPostOn, 1.0, UIEdgeInsets())
|
||||
} else {
|
||||
return (PresentationResourcesChat.chatInputTextFieldSilentPostOffImage(theme), nil, 1.0, UIEdgeInsets())
|
||||
return (PresentationResourcesChat.chatInputTextFieldSilentPostOffImage(theme), nil, strings.VoiceOver_SilentPostOff, 1.0, UIEdgeInsets())
|
||||
}
|
||||
case let .messageAutoremoveTimeout(timeout):
|
||||
if let timeout = timeout {
|
||||
return (nil, shortTimeIntervalString(strings: strings, value: timeout), 1.0, UIEdgeInsets())
|
||||
return (nil, shortTimeIntervalString(strings: strings, value: timeout), strings.VoiceOver_SelfDestructTimerOn(timeIntervalString(strings: strings, value: timeout)).0, 1.0, UIEdgeInsets())
|
||||
} else {
|
||||
return (PresentationResourcesChat.chatInputTextFieldTimerImage(theme), nil, 1.0, UIEdgeInsets(top: 0.0, left: 0.0, bottom: 1.0, right: 0.0))
|
||||
return (PresentationResourcesChat.chatInputTextFieldTimerImage(theme), nil, strings.VoiceOver_SelfDestructTimerOff, 1.0, UIEdgeInsets(top: 0.0, left: 0.0, bottom: 1.0, right: 0.0))
|
||||
}
|
||||
case .scheduledMessages:
|
||||
return (PresentationResourcesChat.chatInputTextFieldScheduleImage(theme), nil, 1.0, UIEdgeInsets())
|
||||
return (PresentationResourcesChat.chatInputTextFieldScheduleImage(theme), nil, strings.VoiceOver_ScheduledMessages, 1.0, UIEdgeInsets())
|
||||
}
|
||||
}
|
||||
|
||||
@ -220,7 +220,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
var audioRecordingCancelIndicator: ChatTextInputAudioRecordingCancelIndicator?
|
||||
var animatingBinNode: AnimationNode?
|
||||
|
||||
private var accessoryItemButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButton)] = []
|
||||
private var accessoryItemButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)] = []
|
||||
|
||||
private var validLayout: (CGFloat, CGFloat, CGFloat, UIEdgeInsets, CGFloat, LayoutMetrics, Bool)?
|
||||
|
||||
@ -316,9 +316,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
}
|
||||
|
||||
if updateAccessoryButtons {
|
||||
var updatedButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButton)] = []
|
||||
var updatedButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)] = []
|
||||
for item in accessoryItems {
|
||||
var itemAndButton: (ChatTextInputAccessoryItem, AccessoryItemIconButton)?
|
||||
var itemAndButton: (ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)?
|
||||
for i in 0 ..< self.accessoryItemButtons.count {
|
||||
if self.accessoryItemButtons[i].0 == item {
|
||||
itemAndButton = self.accessoryItemButtons[i]
|
||||
@ -327,14 +327,14 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
}
|
||||
}
|
||||
if itemAndButton == nil {
|
||||
let button = AccessoryItemIconButton(item: item, theme: currentState.theme, strings: currentState.strings)
|
||||
button.addTarget(self, action: #selector(self.accessoryItemButtonPressed(_:)), for: [.touchUpInside])
|
||||
let button = AccessoryItemIconButtonNode(item: item, theme: currentState.theme, strings: currentState.strings)
|
||||
button.addTarget(self, action: #selector(self.accessoryItemButtonPressed(_:)), forControlEvents: .touchUpInside)
|
||||
itemAndButton = (item, button)
|
||||
}
|
||||
updatedButtons.append(itemAndButton!)
|
||||
}
|
||||
for (_, button) in self.accessoryItemButtons {
|
||||
button.removeFromSuperview()
|
||||
button.removeFromSupernode()
|
||||
}
|
||||
self.accessoryItemButtons = updatedButtons
|
||||
}
|
||||
@ -401,6 +401,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
self.textPlaceholderNode.isUserInteractionEnabled = false
|
||||
self.attachmentButton = HighlightableButtonNode(pointerStyle: .circle)
|
||||
self.attachmentButton.accessibilityLabel = presentationInterfaceState.strings.VoiceOver_AttachMedia
|
||||
self.attachmentButton.accessibilityTraits = [.button]
|
||||
self.attachmentButton.isAccessibilityElement = true
|
||||
self.attachmentButtonDisabledNode = HighlightableButtonNode()
|
||||
self.searchLayoutClearButton = HighlightableButton()
|
||||
@ -716,6 +717,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
}
|
||||
transition.updateAlpha(layer: self.attachmentButton.layer, alpha: isMediaEnabled ? 1.0 : 0.4)
|
||||
self.attachmentButton.isEnabled = isMediaEnabled
|
||||
self.attachmentButton.accessibilityTraits = (!isSlowmodeActive || isMediaEnabled) ? [.button] : [.button, .notEnabled]
|
||||
self.attachmentButtonDisabledNode.isHidden = !isSlowmodeActive || isMediaEnabled
|
||||
|
||||
if self.presentationInterfaceState != interfaceState {
|
||||
@ -902,11 +904,11 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
updateAccessoryButtons = true
|
||||
}
|
||||
|
||||
var removeAccessoryButtons: [AccessoryItemIconButton]?
|
||||
var removeAccessoryButtons: [AccessoryItemIconButtonNode]?
|
||||
if updateAccessoryButtons {
|
||||
var updatedButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButton)] = []
|
||||
var updatedButtons: [(ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)] = []
|
||||
for item in interfaceState.inputTextPanelState.accessoryItems {
|
||||
var itemAndButton: (ChatTextInputAccessoryItem, AccessoryItemIconButton)?
|
||||
var itemAndButton: (ChatTextInputAccessoryItem, AccessoryItemIconButtonNode)?
|
||||
for i in 0 ..< self.accessoryItemButtons.count {
|
||||
if self.accessoryItemButtons[i].0 == item {
|
||||
itemAndButton = self.accessoryItemButtons[i]
|
||||
@ -915,8 +917,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
}
|
||||
}
|
||||
if itemAndButton == nil {
|
||||
let button = AccessoryItemIconButton(item: item, theme: interfaceState.theme, strings: interfaceState.strings)
|
||||
button.addTarget(self, action: #selector(self.accessoryItemButtonPressed(_:)), for: [.touchUpInside])
|
||||
let button = AccessoryItemIconButtonNode(item: item, theme: interfaceState.theme, strings: interfaceState.strings)
|
||||
button.addTarget(self, action: #selector(self.accessoryItemButtonPressed(_:)), forControlEvents: .touchUpInside)
|
||||
itemAndButton = (item, button)
|
||||
}
|
||||
updatedButtons.append(itemAndButton!)
|
||||
@ -928,7 +930,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
}
|
||||
removeAccessoryButtons!.append(button)
|
||||
} else {
|
||||
button.removeFromSuperview()
|
||||
button.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
self.accessoryItemButtons = updatedButtons
|
||||
@ -1341,8 +1343,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
let buttonSize = CGSize(width: button.buttonWidth, height: minimalInputHeight)
|
||||
button.updateLayout(size: buttonSize)
|
||||
let buttonFrame = CGRect(origin: CGPoint(x: nextButtonTopRight.x - buttonSize.width, y: nextButtonTopRight.y + floor((minimalInputHeight - buttonSize.height) / 2.0)), size: buttonSize)
|
||||
if button.superview == nil {
|
||||
self.view.addSubview(button)
|
||||
if button.supernode == nil {
|
||||
self.addSubnode(button)
|
||||
button.frame = buttonFrame.offsetBy(dx: -additionalOffset, dy: 0.0)
|
||||
transition.updateFrame(layer: button.layer, frame: buttonFrame)
|
||||
if animatedTransition {
|
||||
@ -1362,7 +1364,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate {
|
||||
transition.updateFrame(layer: button.layer, frame: buttonFrame)
|
||||
button.layer.animateScale(from: 1.0, to: 0.2, duration: 0.25, removeOnCompletion: false)
|
||||
button.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak button] _ in
|
||||
button?.removeFromSuperview()
|
||||
button?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -378,7 +378,9 @@ final class ChatTitleView: UIView, NavigationBarTitleView {
|
||||
let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor)
|
||||
state = .info(string, .generic)
|
||||
} else if let user = peer as? TelegramUser {
|
||||
if servicePeer {
|
||||
if user.isDeleted {
|
||||
state = .none
|
||||
} else if servicePeer {
|
||||
let string = NSAttributedString(string: "", font: Font.regular(13.0), textColor: titleTheme.rootController.navigationBar.secondaryTextColor)
|
||||
state = .info(string, .generic)
|
||||
} else if user.flags.contains(.isSupport) {
|
||||
|
@ -581,6 +581,25 @@ func openExternalUrlImpl(context: AccountContext, urlContext: OpenURLContext, ur
|
||||
convertedUrl = "https://t.me/addtheme/\(parameter)"
|
||||
}
|
||||
}
|
||||
} else if parsedUrl.host == "privatepost" {
|
||||
if let components = URLComponents(string: "/?" + query) {
|
||||
var channelId: Int64?
|
||||
var postId: Int32?
|
||||
if let queryItems = components.queryItems {
|
||||
for queryItem in queryItems {
|
||||
if let value = queryItem.value {
|
||||
if queryItem.name == "channel" {
|
||||
channelId = Int64(value)
|
||||
} else if queryItem.name == "post" {
|
||||
postId = Int32(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let channelId = channelId, let postId = postId {
|
||||
convertedUrl = "https://t.me/c/\(channelId)/\(postId)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if parsedUrl.host == "resolve" {
|
||||
|
@ -826,7 +826,7 @@ final class PeerInfoVisualMediaPaneNode: ASDisplayNode, PeerInfoPaneNode, UIScro
|
||||
|
||||
self.scrollNode.view.delaysContentTouches = false
|
||||
self.scrollNode.view.canCancelContentTouches = true
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = true
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||
if #available(iOS 11.0, *) {
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
|
@ -994,6 +994,7 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
|
||||
var hasDiscussion = false
|
||||
var hasVoiceChat = false
|
||||
var displayMore = true
|
||||
var canStartVoiceChat = false
|
||||
if let cachedChannelData = cachedData as? CachedChannelData {
|
||||
canViewStats = cachedChannelData.flags.contains(.canViewStats)
|
||||
}
|
||||
@ -1016,6 +1017,12 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
|
||||
if channel.flags.contains(.isCreator) || channel.hasPermission(.inviteMembers) {
|
||||
result.append(.addMember)
|
||||
}
|
||||
if channel.flags.contains(.hasVoiceChat) {
|
||||
hasVoiceChat = true
|
||||
}
|
||||
if !hasVoiceChat && (channel.flags.contains(.isCreator) || channel.hasPermission(.manageCalls)) {
|
||||
canStartVoiceChat = true
|
||||
}
|
||||
}
|
||||
switch channel.participationStatus {
|
||||
case .member:
|
||||
@ -1046,7 +1053,7 @@ func peerInfoHeaderButtons(peer: Peer?, cachedData: CachedPeerData?, isOpenedFro
|
||||
if channel.isVerified || channel.adminRights != nil || channel.flags.contains(.isCreator) {
|
||||
canReport = false
|
||||
}
|
||||
if !canReport && !canViewStats {
|
||||
if !canReport && !canViewStats && !canStartVoiceChat {
|
||||
displayMore = false
|
||||
}
|
||||
if displayMore {
|
||||
|
@ -2437,6 +2437,7 @@ final class PeerInfoHeaderEditingContentNode: ASDisplayNode {
|
||||
self.addSubnode(self.avatarButtonNode)
|
||||
}
|
||||
self.avatarTextNode.attributedText = NSAttributedString(string: presentationData.strings.Settings_SetNewProfilePhotoOrVideo, font: Font.regular(17.0), textColor: presentationData.theme.list.itemAccentColor)
|
||||
self.avatarButtonNode.accessibilityLabel = self.avatarTextNode.attributedText?.string
|
||||
|
||||
let avatarTextSize = self.avatarTextNode.updateLayout(CGSize(width: width, height: 32.0))
|
||||
transition.updateFrame(node: self.avatarTextNode, frame: CGRect(origin: CGPoint(), size: avatarTextSize))
|
||||
|
Loading…
x
Reference in New Issue
Block a user