diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index b301e795c7..3ffa612b48 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -505,6 +505,18 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal return } strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true, { $0.updatedInterfaceState { $0.withToggledSelectedMessages(ids, value: value) } }) + if let selectionState = strongSelf.presentationInterfaceState.interfaceState.selectionState { + let count = selectionState.selectedIds.count + let text: String + if count == 1 { + text = "1 message selected" + } else { + text = "\(count) messages selected" + } + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1, execute: { + UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, text as NSString) + }) + } }, sendMessage: { [weak self] text in guard let strongSelf = self, canSendMessagesToChat(strongSelf.presentationInterfaceState) else { return @@ -2190,6 +2202,17 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal }, beginMessageSelection: { [weak self] messageIds in if let strongSelf = self, strongSelf.isNodeLoaded { strongSelf.updateChatPresentationInterfaceState(animated: true, interactive: true,{ $0.updatedInterfaceState { $0.withUpdatedSelectedMessages(messageIds) } }) + + if let selectionState = strongSelf.presentationInterfaceState.interfaceState.selectionState { + let count = selectionState.selectedIds.count + let text: String + if count == 1 { + text = "1 message selected" + } else { + text = "\(count) messages selected" + } + UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, text) + } } }, deleteSelectedMessages: { [weak self] in if let strongSelf = self { diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index d2e7aad8c8..39f8420b7b 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -1795,6 +1795,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { }) self.messageActionSheetController = nil self.messageActionSheetControllerAdditionalInset = nil + self.accessibilityElementsHidden = false } if let stableId = self.controllerInteraction.contextHighlightedState?.messageStableId { let contextMenuController = displayContextMenuController?.0 @@ -1803,6 +1804,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { contextMenuController?.dismiss() }, associatedController: contextMenuController) self.messageActionSheetController = (controller, stableId) + self.accessibilityElementsHidden = true if let sheetActions = sheetActions, !sheetActions.isEmpty { self.controllerInteraction.presentGlobalOverlayController(controller, nil) } diff --git a/TelegramUI/ChatInterfaceStateContextMenus.swift b/TelegramUI/ChatInterfaceStateContextMenus.swift index 2161242ce8..652c1739f9 100644 --- a/TelegramUI/ChatInterfaceStateContextMenus.swift +++ b/TelegramUI/ChatInterfaceStateContextMenus.swift @@ -599,14 +599,16 @@ private func canPerformDeleteActions(limits: LimitsConfiguration, accountPeerId: return true } - let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) - if message.id.peerId.namespace == Namespaces.Peer.CloudUser { - if message.timestamp + limits.maxMessageRevokeIntervalInPrivateChats > timestamp { - return true - } - } else { - if message.timestamp + limits.maxMessageRevokeInterval > timestamp { - return true + if message.flags.contains(.Incoming) { + let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) + if message.id.peerId.namespace == Namespaces.Peer.CloudUser { + if message.timestamp + limits.maxMessageRevokeIntervalInPrivateChats > timestamp { + return true + } + } else { + if message.timestamp + limits.maxMessageRevokeInterval > timestamp { + return true + } } } diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index a28fd8f725..5fdd0a212c 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -141,6 +141,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { private var actionButtonsNode: ChatMessageActionButtonsNode? private var shareButtonNode: HighlightableButtonNode? + + private let messageAccessibilityArea: AccessibilityAreaNode private var backgroundType: ChatMessageBackgroundType? private var highlightedState: Bool = false @@ -164,10 +166,12 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { required init() { self.backgroundNode = ChatMessageBackground() + self.messageAccessibilityArea = AccessibilityAreaNode() super.init(layerBacked: false) self.addSubnode(self.backgroundNode) + self.addSubnode(self.messageAccessibilityArea) } required init?(coder aDecoder: NSCoder) { @@ -317,8 +321,10 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { let currentItem = self.appliedItem let currentForwardInfo = self.appliedForwardInfo + let isSelected = self.selectionNode?.selected + return { item, params, mergedTop, mergedBottom, dateHeaderAtBottom in - let accessibilityData = ChatMessageAccessibilityData(item: item) + let accessibilityData = ChatMessageAccessibilityData(item: item, isSelected: isSelected) let baseWidth = params.width - params.leftInset - params.rightInset @@ -1157,8 +1163,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if let strongSelf = self { strongSelf.appliedItem = item strongSelf.appliedForwardInfo = (forwardSource, forwardAuthorSignature) - strongSelf.accessibilityLabel = accessibilityData.label - strongSelf.accessibilityValue = accessibilityData.value + strongSelf.updateAccessibilityData(accessibilityData) var transition: ContainedViewLayoutTransition = .immediate if case let .System(duration) = animation { @@ -1197,7 +1202,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } }) strongSelf.deliveryFailedNode = deliveryFailedNode - strongSelf.addSubnode(deliveryFailedNode) + strongSelf.insertSubnode(deliveryFailedNode, belowSubnode: strongSelf.messageAccessibilityArea) } let deliveryFailedSize = deliveryFailedNode.updateLayout(theme: item.presentationData.theme.theme) let deliveryFailedFrame = CGRect(origin: CGPoint(x: backgroundFrame.maxX + deliveryFailedInset - deliveryFailedSize.width, y: backgroundFrame.maxY - deliveryFailedSize.height), size: deliveryFailedSize) @@ -1221,7 +1226,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if !nameNode.isNodeLoaded { nameNode.isUserInteractionEnabled = false } - strongSelf.addSubnode(nameNode) + strongSelf.insertSubnode(nameNode, belowSubnode: strongSelf.messageAccessibilityArea) } nameNode.frame = CGRect(origin: CGPoint(x: contentOrigin.x + layoutConstants.text.bubbleInsets.left, y: layoutConstants.bubble.contentInsets.top + nameNodeOriginY), size: nameNodeSizeApply.0) @@ -1232,7 +1237,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if !adminBadgeNode.isNodeLoaded { adminBadgeNode.isUserInteractionEnabled = false } - strongSelf.addSubnode(adminBadgeNode) + strongSelf.insertSubnode(adminBadgeNode, belowSubnode: strongSelf.messageAccessibilityArea) adminBadgeNode.frame = adminBadgeFrame } else { let previousAdminBadgeFrame = adminBadgeNode.frame @@ -1254,7 +1259,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { strongSelf.forwardInfoNode = forwardInfoNode var animateFrame = true if forwardInfoNode.supernode == nil { - strongSelf.addSubnode(forwardInfoNode) + strongSelf.insertSubnode(forwardInfoNode, belowSubnode: strongSelf.messageAccessibilityArea) animateFrame = false } let previousForwardInfoNodeFrame = forwardInfoNode.frame @@ -1273,7 +1278,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { strongSelf.replyInfoNode = replyInfoNode var animateFrame = true if replyInfoNode.supernode == nil { - strongSelf.addSubnode(replyInfoNode) + strongSelf.insertSubnode(replyInfoNode, belowSubnode: strongSelf.messageAccessibilityArea) animateFrame = false } let previousReplyInfoNodeFrame = replyInfoNode.frame @@ -1306,9 +1311,9 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } if let addedContentNodes = addedContentNodes { - for (contentNodeMessage, contentNode) in addedContentNodes { + for (_, contentNode) in addedContentNodes { updatedContentNodes.append(contentNode) - strongSelf.addSubnode(contentNode) + strongSelf.insertSubnode(contentNode, belowSubnode: strongSelf.messageAccessibilityArea) contentNode.visibility = strongSelf.visibility } @@ -1376,7 +1381,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if mosaicStatusNode !== strongSelf.mosaicStatusNode { strongSelf.mosaicStatusNode?.removeFromSupernode() strongSelf.mosaicStatusNode = mosaicStatusNode - strongSelf.addSubnode(mosaicStatusNode) + strongSelf.insertSubnode(mosaicStatusNode, belowSubnode: strongSelf.messageAccessibilityArea) } let absoluteOrigin = mosaicStatusOrigin.offsetBy(dx: contentOrigin.x, dy: contentOrigin.y) mosaicStatusNode.frame = CGRect(origin: CGPoint(x: absoluteOrigin.x - layoutConstants.image.statusInsets.right - size.width, y: absoluteOrigin.y - layoutConstants.image.statusInsets.bottom - size.height), size: size) @@ -1391,7 +1396,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { shareButtonNode.removeFromSupernode() } strongSelf.shareButtonNode = updatedShareButtonNode - strongSelf.addSubnode(updatedShareButtonNode) + strongSelf.insertSubnode(updatedShareButtonNode, belowSubnode: strongSelf.messageAccessibilityArea) updatedShareButtonNode.addTarget(strongSelf, action: #selector(strongSelf.shareButtonPressed), forControlEvents: .touchUpInside) } if let updatedShareButtonBackground = updatedShareButtonBackground { @@ -1417,6 +1422,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { strongSelf.backgroundFrameTransition = nil } strongSelf.backgroundNode.frame = backgroundFrame + strongSelf.messageAccessibilityArea.frame = backgroundFrame if let shareButtonNode = strongSelf.shareButtonNode { shareButtonNode.frame = CGRect(origin: CGPoint(x: backgroundFrame.maxX + 8.0, y: backgroundFrame.maxY - 30.0), size: CGSize(width: 29.0, height: 29.0)) } @@ -1448,7 +1454,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { strongSelf.presentMessageButtonContextMenu(button: button) } } - strongSelf.addSubnode(actionButtonsNode) + strongSelf.insertSubnode(actionButtonsNode, belowSubnode: strongSelf.messageAccessibilityArea) } else { if case let .System(duration) = animation { actionButtonsNode.layer.animateFrame(from: previousFrame, to: actionButtonsFrame, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) @@ -1465,11 +1471,18 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } } + private func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { + self.messageAccessibilityArea.accessibilityLabel = accessibilityData.label + self.messageAccessibilityArea.accessibilityValue = accessibilityData.value + self.messageAccessibilityArea.accessibilityHint = accessibilityData.hint + self.messageAccessibilityArea.accessibilityTraits = accessibilityData.traits + } + private func addContentNode(node: ChatMessageBubbleContentNode) { if let transitionClippingNode = self.transitionClippingNode { transitionClippingNode.addSubnode(node) } else { - self.addSubnode(node) + self.insertSubnode(node, belowSubnode: self.messageAccessibilityArea) } } @@ -1490,7 +1503,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { for contentNode in self.contentNodes { node.addSubnode(contentNode) } - self.addSubnode(node) + self.insertSubnode(node, belowSubnode: self.messageAccessibilityArea) self.transitionClippingNode = node } } @@ -1498,13 +1511,13 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { private func disableTransitionClippingNode() { if let transitionClippingNode = self.transitionClippingNode { if let forwardInfoNode = self.forwardInfoNode { - self.addSubnode(forwardInfoNode) + self.insertSubnode(forwardInfoNode, belowSubnode: self.messageAccessibilityArea) } if let replyInfoNode = self.replyInfoNode { - self.addSubnode(replyInfoNode) + self.insertSubnode(replyInfoNode, belowSubnode: self.messageAccessibilityArea) } for contentNode in self.contentNodes { - self.addSubnode(contentNode) + self.insertSubnode(contentNode, belowSubnode: self.messageAccessibilityArea) } transitionClippingNode.removeFromSupernode() self.transitionClippingNode = nil @@ -1525,6 +1538,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { if let backgroundFrameTransition = self.backgroundFrameTransition { let backgroundFrame = CGRect.interpolator()(backgroundFrameTransition.0, backgroundFrameTransition.1, progress) as! CGRect self.backgroundNode.frame = backgroundFrame + self.messageAccessibilityArea.frame = backgroundFrame if let shareButtonNode = self.shareButtonNode { shareButtonNode.frame = CGRect(origin: CGPoint(x: backgroundFrame.maxX + 8.0, y: backgroundFrame.maxY - 30.0), size: CGSize(width: 29.0, height: 29.0)) @@ -1881,6 +1895,8 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { return } + let wasSelected = self.selectionNode?.selected + var canHaveSelection = true switch item.content { case let .message(message, _, _, _): @@ -1935,7 +1951,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { }) selectionNode.frame = CGRect(origin: CGPoint(x: -offset, y: 0.0), size: CGSize(width: self.contentBounds.size.width, height: self.contentBounds.size.height)) - self.addSubnode(selectionNode) + self.insertSubnode(selectionNode, belowSubnode: self.messageAccessibilityArea) self.selectionNode = selectionNode selectionNode.updateSelected(selected, animated: false) let previousSubnodeTransform = self.subnodeTransform @@ -1969,6 +1985,11 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } } } + + let isSelected = self.selectionNode?.selected + if wasSelected != isSelected { + self.updateAccessibilityData(ChatMessageAccessibilityData(item: item, isSelected: isSelected)) + } } override func updateSearchTextHighlightState() { @@ -2055,7 +2076,7 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { let swipeToReplyNode = ChatMessageSwipeToReplyNode(fillColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.bubble.shareButtonFillColor, wallpaper: item.presentationData.theme.wallpaper), strokeColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.bubble.shareButtonStrokeColor, wallpaper: item.presentationData.theme.wallpaper), foregroundColor: bubbleVariableColor(variableColor: item.presentationData.theme.theme.chat.bubble.shareButtonForegroundColor, wallpaper: item.presentationData.theme.wallpaper)) self.swipeToReplyNode = swipeToReplyNode - self.addSubnode(swipeToReplyNode) + self.insertSubnode(swipeToReplyNode, belowSubnode: self.messageAccessibilityArea) animateReplyNodeIn = true } } diff --git a/TelegramUI/ChatMessageItemView.swift b/TelegramUI/ChatMessageItemView.swift index 1c20b6dd9f..6593cf0f6e 100644 --- a/TelegramUI/ChatMessageItemView.swift +++ b/TelegramUI/ChatMessageItemView.swift @@ -98,35 +98,131 @@ enum ChatMessagePeekPreviewContent { case url(ASDisplayNode, CGRect, String) } +private let voiceMessageDurationFormatter: DateComponentsFormatter = { + let formatter = DateComponentsFormatter() + formatter.unitsStyle = .spellOut + formatter.allowedUnits = [.second] + formatter.zeroFormattingBehavior = .pad + return formatter +}() + +private let musicDurationFormatter: DateComponentsFormatter = { + let formatter = DateComponentsFormatter() + formatter.unitsStyle = .spellOut + formatter.allowedUnits = [.minute, .second] + formatter.zeroFormattingBehavior = .pad + return formatter +}() + final class ChatMessageAccessibilityData { let label: String? let value: String? + let hint: String? + let traits: UIAccessibilityTraits - init(item: ChatMessageItem) { - let label: String + init(item: ChatMessageItem, isSelected: Bool?) { + var label: String let value: String + var hint: String? + var traits: UIAccessibilityTraits = 0 + let isIncoming = item.message.effectivelyIncoming(item.context.account.peerId) + var announceIncomingAuthors = false + if let peer = item.message.peers[item.message.id.peerId] { + if peer is TelegramGroup { + announceIncomingAuthors = true + } else if let channel = peer as? TelegramChannel, case .group = channel.info { + announceIncomingAuthors = true + } + } + + var authorName: String? if let author = item.message.author { - if item.message.effectivelyIncoming(item.context.account.peerId) { + authorName = author.displayTitle + if isIncoming { label = author.displayTitle } else { - label = "Outgoing message" + label = "Your message" } } else { - label = "Post" + label = "Message" } if let chatPeer = item.message.peers[item.message.id.peerId] { let (_, _, messageText) = chatListItemStrings(strings: item.presentationData.strings, nameDisplayOrder: item.presentationData.nameDisplayOrder, message: item.message, chatPeer: RenderedPeer(peer: chatPeer), accountPeerId: item.context.account.peerId) + + var text = messageText + + loop: for media in item.message.media { + if let file = media as? TelegramMediaFile { + for attribute in file.attributes { + switch attribute { + case let .Audio(audio): + if isSelected == nil { + hint = "Double tap to play" + } + traits |= UIAccessibilityTraitStartsMediaSession + if audio.isVoice { + let durationString = voiceMessageDurationFormatter.string(from: Double(audio.duration)) ?? "" + if isIncoming { + if announceIncomingAuthors, let authorName = authorName { + label = "Voice message, from: \(authorName)" + } else { + label = "Voice message" + } + } else { + label = "Your voice message" + } + text = "Duration: \(durationString)" + } else { + let durationString = musicDurationFormatter.string(from: Double(audio.duration)) ?? "" + if announceIncomingAuthors, let authorName = authorName { + label = "Music file, from: \(authorName)" + } else { + label = "Your music file" + } + let performer = audio.performer ?? "Unknown" + let title = audio.title ?? "Unknown" + text = "\(title), by \(performer). Duration: \(durationString)" + } + default: + break + } + } + break loop + } + } + var result = "" - result += "\(messageText)" + + if let isSelected = isSelected { + if isSelected { + result += "Selected.\n" + } + traits |= UIAccessibilityTraitStartsMediaSession + } + + result += "\(text)" + + let dateString = DateFormatter.localizedString(from: Date(timeIntervalSince1970: Double(item.message.timestamp)), dateStyle: DateFormatter.Style.medium, timeStyle: DateFormatter.Style.short) + + result += "\n\(dateString)" + if !isIncoming && item.read { + if announceIncomingAuthors { + result += "Seen by recipients" + } else { + result += "Seen by recipient" + } + } value = result } else { - value = "Empty" + value = "" } self.label = label self.value = value + self.hint = hint + self.traits = traits } } @@ -142,8 +238,6 @@ public class ChatMessageItemView: ListViewItemNode { public init(layerBacked: Bool) { super.init(layerBacked: layerBacked, dynamicBounce: true, rotated: true) self.transform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0) - - self.isAccessibilityElement = true } required public init?(coder aDecoder: NSCoder) { diff --git a/TelegramUI/ChatMessageSelectionNode.swift b/TelegramUI/ChatMessageSelectionNode.swift index 733d39eddb..8f7d1f9969 100644 --- a/TelegramUI/ChatMessageSelectionNode.swift +++ b/TelegramUI/ChatMessageSelectionNode.swift @@ -4,7 +4,7 @@ import AsyncDisplayKit final class ChatMessageSelectionNode: ASDisplayNode { private let toggle: (Bool) -> Void - private var selected = false + private(set) var selected = false private let checkNode: CheckNode init(theme: PresentationTheme, toggle: @escaping (Bool) -> Void) { diff --git a/TelegramUI/ChatRecordingPreviewInputPanelNode.swift b/TelegramUI/ChatRecordingPreviewInputPanelNode.swift index ac30eaa36a..bfac1cc7c4 100644 --- a/TelegramUI/ChatRecordingPreviewInputPanelNode.swift +++ b/TelegramUI/ChatRecordingPreviewInputPanelNode.swift @@ -50,12 +50,15 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { self.playButton = HighlightableButtonNode() self.playButton.displaysAsynchronously = false self.playButton.setImage(generatePlayIcon(theme), for: []) + self.playButton.isUserInteractionEnabled = false self.pauseButton = HighlightableButtonNode() self.pauseButton.displaysAsynchronously = false self.pauseButton.setImage(generatePauseIcon(theme), for: []) self.pauseButton.isHidden = true + self.pauseButton.isUserInteractionEnabled = false self.waveformButton = ASButtonNode() + self.waveformButton.accessibilityTraits |= UIAccessibilityTraitStartsMediaSession self.waveformNode = AudioWaveformNode() self.waveformNode.isLayerBacked = true @@ -100,6 +103,11 @@ final class ChatRecordingPreviewInputPanelNode: ChatInputPanelNode { if self.presentationInterfaceState?.recordedMediaPreview != interfaceState.recordedMediaPreview { updateWaveform = true } + if self.presentationInterfaceState?.strings !== interfaceState.strings { + self.deleteButton.accessibilityLabel = "Delete" + self.sendButton.accessibilityLabel = "Send" + self.waveformButton.accessibilityLabel = "Preview voice message" + } self.presentationInterfaceState = interfaceState if let recordedMediaPreview = interfaceState.recordedMediaPreview, updateWaveform { diff --git a/TelegramUI/ChatTextInputActionButtonsNode.swift b/TelegramUI/ChatTextInputActionButtonsNode.swift index 2c356673c1..7235823775 100644 --- a/TelegramUI/ChatTextInputActionButtonsNode.swift +++ b/TelegramUI/ChatTextInputActionButtonsNode.swift @@ -42,12 +42,20 @@ final class ChatTextInputActionButtonsNode: ASDisplayNode { } func updateAccessibility() { - if self.sendButton.alpha.isZero { - self.accessibilityTraits = UIAccessibilityTraitButton | UIAccessibilityTraitNotEnabled - self.accessibilityLabel = "Send" + if !self.micButton.alpha.isZero { + self.accessibilityTraits = UIAccessibilityTraitButton + switch self.micButton.mode { + case .audio: + self.accessibilityLabel = "Voice Message" + self.accessibilityHint = "Double tap and hold to record voice message. Slide up to pin recording, slide left to cancel. Double tap to switch to video." + case .video: + self.accessibilityLabel = "Video Message" + self.accessibilityHint = "Double tap and hold to record voice message. Slide up to pin recording, slide left to cancel. Double tap to switch to audio." + } } else { self.accessibilityTraits = UIAccessibilityTraitButton self.accessibilityLabel = "Send" + self.accessibilityHint = nil } } } diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index 9315c36784..dc88bb0d2a 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -196,15 +196,16 @@ enum ChatTextInputPanelPasteData { } class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { - var textPlaceholderNode: TextNode + var textPlaceholderNode: ImmediateTextNode var contextPlaceholderNode: TextNode? let textInputContainer: ASDisplayNode var textInputNode: EditableTextNode? let textInputBackgroundView: UIImageView let actionButtons: ChatTextInputActionButtonsNode + var mediaRecordingAccessibilityArea: AccessibilityAreaNode? - let attachmentButton: HighlightableButton + let attachmentButton: HighlightableButtonNode let searchLayoutClearButton: HighlightableButton let searchLayoutProgressView: UIImageView var audioRecordingInfoContainerNode: ASDisplayNode? @@ -321,9 +322,12 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.textInputContainer.backgroundColor = theme.chat.inputPanel.inputBackgroundColor self.textInputBackgroundView = UIImageView() - self.textPlaceholderNode = TextNode() + self.textPlaceholderNode = ImmediateTextNode() + self.textPlaceholderNode.maximumNumberOfLines = 1 self.textPlaceholderNode.isLayerBacked = true - self.attachmentButton = HighlightableButton() + self.attachmentButton = HighlightableButtonNode() + self.attachmentButton.accessibilityLabel = "Send media" + self.attachmentButton.isAccessibilityElement = true self.searchLayoutClearButton = HighlightableButton() self.searchLayoutProgressView = UIImageView(image: searchLayoutProgressImage) self.searchLayoutProgressView.isHidden = true @@ -332,8 +336,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { super.init() - self.attachmentButton.addTarget(self, action: #selector(self.attachmentButtonPressed), for: .touchUpInside) - self.view.addSubview(self.attachmentButton) + self.attachmentButton.addTarget(self, action: #selector(self.attachmentButtonPressed), forControlEvents: .touchUpInside) + self.addSubnode(self.attachmentButton) self.addSubnode(self.actionButtons) @@ -495,6 +499,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } textInputNode.view.addGestureRecognizer(recognizer) + + textInputNode.textView.accessibilityHint = self.textPlaceholderNode.attributedText?.string } private func textFieldMaxHeight(_ maxHeight: CGFloat, metrics: LayoutMetrics) -> CGFloat { @@ -688,7 +694,9 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.currentPlaceholder = placeholder let placeholderLayout = TextNode.asyncLayout(self.textPlaceholderNode) let baseFontSize = max(17.0, interfaceState.fontSize.baseDisplaySize) - let (placeholderSize, placeholderApply) = placeholderLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: placeholder, font: Font.regular(baseFontSize), textColor: interfaceState.theme.chat.inputPanel.inputPlaceholderColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + self.textPlaceholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(baseFontSize), textColor: interfaceState.theme.chat.inputPanel.inputPlaceholderColor) + self.textInputNode?.textView.accessibilityHint = placeholder + let placeholderSize = self.textPlaceholderNode.updateLayout(CGSize(width: 320.0, height: CGFloat.greatestFiniteMagnitude)) if transition.isAnimated, let snapshotLayer = self.textPlaceholderNode.layer.snapshotContentTree() { self.textPlaceholderNode.supernode?.layer.insertSublayer(snapshotLayer, above: self.textPlaceholderNode.layer) snapshotLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.22, removeOnCompletion: false, completion: { [weak snapshotLayer] _ in @@ -696,8 +704,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { }) self.textPlaceholderNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.18) } - self.textPlaceholderNode.frame = CGRect(origin: self.textPlaceholderNode.frame.origin, size: placeholderSize.size) - let _ = placeholderApply() + self.textPlaceholderNode.frame = CGRect(origin: self.textPlaceholderNode.frame.origin, size: placeholderSize) } } @@ -953,11 +960,48 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { textInputBackgroundWidthOffset = 36.0 } - transition.updateFrame(node: self.actionButtons, frame: CGRect(origin: CGPoint(x: width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset, y: panelHeight - minimalHeight), size: CGSize(width: 44.0, height: minimalHeight))) + let actionButtonsFrame = CGRect(origin: CGPoint(x: width - rightInset - 43.0 - UIScreenPixel + composeButtonsOffset, y: panelHeight - minimalHeight), size: CGSize(width: 44.0, height: minimalHeight)) + transition.updateFrame(node: self.actionButtons, frame: actionButtonsFrame) + if let presentationInterfaceState = self.presentationInterfaceState { self.actionButtons.updateLayout(size: CGSize(width: 44.0, height: minimalHeight), transition: transition, interfaceState: presentationInterfaceState) } + if let mediaRecordingState = interfaceState.inputTextPanelState.mediaRecordingState { + let text: String = "Send" + let mediaRecordingAccessibilityArea: AccessibilityAreaNode + var added = false + if let current = self.mediaRecordingAccessibilityArea { + mediaRecordingAccessibilityArea = current + } else { + added = true + mediaRecordingAccessibilityArea = AccessibilityAreaNode() + mediaRecordingAccessibilityArea.accessibilityLabel = text + mediaRecordingAccessibilityArea.accessibilityTraits = UIAccessibilityTraitButton | UIAccessibilityTraitStartsMediaSession + self.mediaRecordingAccessibilityArea = mediaRecordingAccessibilityArea + mediaRecordingAccessibilityArea.activate = { [weak self] in + self?.interfaceInteraction?.finishMediaRecording(.send) + return true + } + self.insertSubnode(mediaRecordingAccessibilityArea, aboveSubnode: self.actionButtons) + } + self.actionButtons.isAccessibilityElement = false + let size: CGFloat = 120.0 + mediaRecordingAccessibilityArea.frame = CGRect(origin: CGPoint(x: actionButtonsFrame.midX - size / 2.0, y: actionButtonsFrame.midY - size / 2.0), size: CGSize(width: size, height: size)) + if added { + DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.4, execute: { + [weak mediaRecordingAccessibilityArea] in + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, mediaRecordingAccessibilityArea?.view) + }) + } + } else { + self.actionButtons.isAccessibilityElement = true + if let mediaRecordingAccessibilityArea = self.mediaRecordingAccessibilityArea { + self.mediaRecordingAccessibilityArea = nil + mediaRecordingAccessibilityArea.removeFromSupernode() + } + } + let searchLayoutClearButtonSize = CGSize(width: 44.0, height: minimalHeight) let textFieldInsets = self.textFieldInsets(metrics: metrics) transition.updateFrame(layer: self.searchLayoutClearButton.layer, frame: CGRect(origin: CGPoint(x: width - rightInset - textFieldInsets.left - textFieldInsets.right + textInputBackgroundWidthOffset + 3.0, y: panelHeight - minimalHeight), size: searchLayoutClearButtonSize)) @@ -1223,6 +1267,8 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { } } } + + self.actionButtons.updateAccessibility() } private func updateTextHeight() { diff --git a/TelegramUI/ItemListActionItem.swift b/TelegramUI/ItemListActionItem.swift index 6d62a57cf2..fef00c7aaf 100644 --- a/TelegramUI/ItemListActionItem.swift +++ b/TelegramUI/ItemListActionItem.swift @@ -89,6 +89,8 @@ class ItemListActionItemNode: ListViewItemNode, ItemListItemNode { private let titleNode: TextNode + private let activateArea: AccessibilityAreaNode + private var item: ItemListActionItem? var tag: ItemListItemTag? { @@ -114,11 +116,13 @@ class ItemListActionItemNode: ListViewItemNode, ItemListItemNode { self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.isLayerBacked = true + self.activateArea = AccessibilityAreaNode() + super.init(layerBacked: false, dynamicBounce: false) - self.isAccessibilityElement = true - self.addSubnode(self.titleNode) + + self.addSubnode(self.activateArea) } func asyncLayout() -> (_ item: ItemListActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { @@ -172,7 +176,9 @@ class ItemListActionItemNode: ListViewItemNode, ItemListItemNode { if let strongSelf = self { strongSelf.item = item - strongSelf.accessibilityLabel = item.title + strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) + strongSelf.activateArea.accessibilityLabel = item.title + var accessibilityTraits: UIAccessibilityTraits = UIAccessibilityTraitButton switch item.kind { case .disabled: @@ -180,7 +186,7 @@ class ItemListActionItemNode: ListViewItemNode, ItemListItemNode { default: break } - strongSelf.accessibilityTraits = accessibilityTraits + strongSelf.activateArea.accessibilityTraits = accessibilityTraits if let _ = updatedTheme { strongSelf.topStripeNode.backgroundColor = itemSeparatorColor diff --git a/TelegramUI/ItemListDisclosureItem.swift b/TelegramUI/ItemListDisclosureItem.swift index 76ea8a5491..f87015ec50 100644 --- a/TelegramUI/ItemListDisclosureItem.swift +++ b/TelegramUI/ItemListDisclosureItem.swift @@ -110,6 +110,8 @@ class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { let labelBadgeNode: ASImageNode let labelImageNode: ASImageNode + private let activateArea: AccessibilityAreaNode + private var item: ItemListDisclosureItem? override var canBeSelected: Bool { @@ -159,13 +161,15 @@ class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.isLayerBacked = true - super.init(layerBacked: false, dynamicBounce: false) + self.activateArea = AccessibilityAreaNode() - self.isAccessibilityElement = true + super.init(layerBacked: false, dynamicBounce: false) self.addSubnode(self.titleNode) self.addSubnode(self.labelNode) self.addSubnode(self.arrowNode) + + self.addSubnode(self.activateArea) } func asyncLayout() -> (_ item: ItemListDisclosureItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { @@ -294,9 +298,14 @@ class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode { if let strongSelf = self { strongSelf.item = item - strongSelf.accessibilityLabel = item.title - strongSelf.accessibilityValue = item.label - strongSelf.accessibilityTraits = UIAccessibilityTraitButton + strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) + strongSelf.activateArea.accessibilityLabel = item.title + strongSelf.activateArea.accessibilityValue = item.label + if item.enabled { + strongSelf.activateArea.accessibilityTraits = 0 + } else { + strongSelf.activateArea.accessibilityTraits = UIAccessibilityTraitNotEnabled + } if let icon = item.icon { if strongSelf.iconNode.supernode == nil { diff --git a/TelegramUI/ItemListMultilineTextItem.swift b/TelegramUI/ItemListMultilineTextItem.swift index 409af195b0..6362f0f9c7 100644 --- a/TelegramUI/ItemListMultilineTextItem.swift +++ b/TelegramUI/ItemListMultilineTextItem.swift @@ -95,6 +95,8 @@ class ItemListMultilineTextItemNode: ListViewItemNode { private let textNode: TextNode + private let activateArea: AccessibilityAreaNode + private var item: ItemListMultilineTextItem? var tag: Any? { @@ -124,9 +126,12 @@ class ItemListMultilineTextItemNode: ListViewItemNode { self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.isLayerBacked = true + self.activateArea = AccessibilityAreaNode() + super.init(layerBacked: false, dynamicBounce: false) self.addSubnode(self.textNode) + self.addSubnode(self.activateArea) } override func didLoad() { @@ -201,6 +206,9 @@ class ItemListMultilineTextItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item + strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) + strongSelf.activateArea.accessibilityLabel = item.text + if let _ = updatedTheme { strongSelf.topStripeNode.backgroundColor = itemSeparatorColor strongSelf.bottomStripeNode.backgroundColor = itemSeparatorColor diff --git a/TelegramUI/ItemListPeerActionItem.swift b/TelegramUI/ItemListPeerActionItem.swift index 2a47a34b29..45d64948a5 100644 --- a/TelegramUI/ItemListPeerActionItem.swift +++ b/TelegramUI/ItemListPeerActionItem.swift @@ -87,6 +87,8 @@ class ItemListPeerActionItemNode: ListViewItemNode { private let iconNode: ASImageNode private let titleNode: TextNode + private let activateArea: AccessibilityAreaNode + private var item: ItemListPeerActionItem? init() { @@ -112,12 +114,14 @@ class ItemListPeerActionItemNode: ListViewItemNode { self.highlightedBackgroundNode = ASDisplayNode() self.highlightedBackgroundNode.isLayerBacked = true - super.init(layerBacked: false, dynamicBounce: false) + self.activateArea = AccessibilityAreaNode() - self.isAccessibilityElement = true + super.init(layerBacked: false, dynamicBounce: false) self.addSubnode(self.iconNode) self.addSubnode(self.titleNode) + + self.addSubnode(self.activateArea) } func asyncLayout() -> (_ item: ItemListPeerActionItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) { @@ -149,7 +153,8 @@ class ItemListPeerActionItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item - strongSelf.accessibilityLabel = item.title + strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) + strongSelf.activateArea.accessibilityLabel = item.title if let _ = updatedTheme { strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor diff --git a/TelegramUI/ItemListPeerItem.swift b/TelegramUI/ItemListPeerItem.swift index 234eac2033..7f9f730c91 100644 --- a/TelegramUI/ItemListPeerItem.swift +++ b/TelegramUI/ItemListPeerItem.swift @@ -505,7 +505,7 @@ class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNode { combinedValueString.append(statusString) } if let labelString = labelAttributedString?.string, !labelString.isEmpty { - combinedValueString.append(labelString) + combinedValueString.append(", \(labelString)") } strongSelf.accessibilityValue = combinedValueString diff --git a/TelegramUI/ItemListSectionHeaderItem.swift b/TelegramUI/ItemListSectionHeaderItem.swift index 889c4cf971..df68598bc3 100644 --- a/TelegramUI/ItemListSectionHeaderItem.swift +++ b/TelegramUI/ItemListSectionHeaderItem.swift @@ -73,6 +73,8 @@ class ItemListSectionHeaderItemNode: ListViewItemNode { private let titleNode: TextNode private let accessoryTextNode: TextNode + private let activateArea: AccessibilityAreaNode + init() { self.titleNode = TextNode() self.titleNode.isUserInteractionEnabled = false @@ -84,12 +86,14 @@ class ItemListSectionHeaderItemNode: ListViewItemNode { self.accessoryTextNode.contentMode = .left self.accessoryTextNode.contentsScale = UIScreen.main.scale - super.init(layerBacked: false, dynamicBounce: false) + self.activateArea = AccessibilityAreaNode() + self.activateArea.accessibilityTraits = UIAccessibilityTraitStaticText | UIAccessibilityTraitHeader - self.isAccessibilityElement = true + super.init(layerBacked: false, dynamicBounce: false) self.addSubnode(self.titleNode) self.addSubnode(self.accessoryTextNode) + self.addSubnode(self.activateArea) } func asyncLayout() -> (_ item: ItemListSectionHeaderItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { @@ -133,7 +137,8 @@ class ItemListSectionHeaderItemNode: ListViewItemNode { let _ = titleApply() let _ = accessoryApply() - strongSelf.accessibilityLabel = item.text + strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) + strongSelf.activateArea.accessibilityLabel = item.text strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 7.0), size: titleLayout.size) strongSelf.accessoryTextNode.frame = CGRect(origin: CGPoint(x: params.width - leftInset - accessoryLayout.size.width, y: 7.0), size: accessoryLayout.size) diff --git a/TelegramUI/ItemListSwitchItem.swift b/TelegramUI/ItemListSwitchItem.swift index 9952349cda..f91a150334 100644 --- a/TelegramUI/ItemListSwitchItem.swift +++ b/TelegramUI/ItemListSwitchItem.swift @@ -81,6 +81,9 @@ private protocol ItemListSwitchNodeImpl { var handleColor: UIColor { get set } var positiveContentColor: UIColor { get set } var negativeContentColor: UIColor { get set } + + var isOn: Bool { get } + func setOn(_ value: Bool, animated: Bool) } extension SwitchNode: ItemListSwitchNodeImpl { @@ -114,6 +117,8 @@ class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { private let switchGestureNode: ASDisplayNode private var disabledOverlayNode: ASDisplayNode? + private let activateArea: AccessibilityAreaNode + private var item: ItemListSwitchItem? var tag: ItemListItemTag? { @@ -145,13 +150,26 @@ class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { self.switchGestureNode = ASDisplayNode() - super.init(layerBacked: false, dynamicBounce: false) + self.activateArea = AccessibilityAreaNode() - self.isAccessibilityElement = true + super.init(layerBacked: false, dynamicBounce: false) self.addSubnode(self.titleNode) self.addSubnode(self.switchNode) self.addSubnode(self.switchGestureNode) + self.addSubnode(self.activateArea) + + self.activateArea.activate = { [weak self] in + guard let strongSelf = self, let item = strongSelf.item, item.enabled else { + return false + } + let value = !strongSelf.switchNode.isOn + if item.enableInteractiveChanges { + strongSelf.switchNode.setOn(value, animated: true) + } + item.updated(value) + return true + } } override func didLoad() { @@ -213,14 +231,17 @@ class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode { if let strongSelf = self { strongSelf.item = item - strongSelf.accessibilityLabel = item.title - strongSelf.accessibilityValue = item.value ? "On" : "Off" + strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) + + strongSelf.activateArea.accessibilityLabel = item.title + strongSelf.activateArea.accessibilityValue = item.value ? "On" : "Off" + strongSelf.activateArea.accessibilityHint = "Tap to change" var accessibilityTraits = UIAccessibilityTraits() if item.enabled { } else { accessibilityTraits |= UIAccessibilityTraitNotEnabled } - strongSelf.accessibilityTraits = accessibilityTraits + strongSelf.activateArea.accessibilityTraits = accessibilityTraits let transition: ContainedViewLayoutTransition if animated { diff --git a/TelegramUI/ItemListTextItem.swift b/TelegramUI/ItemListTextItem.swift index 2fbb24e5b4..357afd71d1 100644 --- a/TelegramUI/ItemListTextItem.swift +++ b/TelegramUI/ItemListTextItem.swift @@ -70,6 +70,7 @@ private let titleBoldFont = Font.semibold(14.0) class ItemListTextItemNode: ListViewItemNode { private let titleNode: TextNode + private let activateArea: AccessibilityAreaNode private var item: ItemListTextItem? @@ -79,12 +80,13 @@ class ItemListTextItemNode: ListViewItemNode { self.titleNode.contentMode = .left self.titleNode.contentsScale = UIScreen.main.scale + self.activateArea = AccessibilityAreaNode() + self.activateArea.accessibilityTraits = UIAccessibilityTraitStaticText + super.init(layerBacked: false, dynamicBounce: false) - self.isAccessibilityElement = true - self.accessibilityTraits = UIAccessibilityTraitStaticText - self.addSubnode(self.titleNode) + self.addSubnode(self.activateArea) } override func didLoad() { @@ -126,6 +128,9 @@ class ItemListTextItemNode: ListViewItemNode { if let strongSelf = self { strongSelf.item = item + strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: params.width - params.leftInset - params.rightInset, height: layout.contentSize.height)) + strongSelf.activateArea.accessibilityLabel = attributedText.string + strongSelf.accessibilityLabel = attributedText.string let _ = titleApply()