From f2d8c13efaf360e3192c3d87cd452dcb95b2c2d1 Mon Sep 17 00:00:00 2001 From: Peter <> Date: Tue, 19 Mar 2019 18:55:17 +0400 Subject: [PATCH] Accessibility updates --- ...ceAwaitingAccountResetControllerNode.swift | 4 +- TelegramUI/ChannelInfoController.swift | 2 +- TelegramUI/ChannelVisibilityController.swift | 2 +- TelegramUI/ChatController.swift | 1 + TelegramUI/ChatControllerNode.swift | 1 + .../ChatInterfaceStateContextMenus.swift | 8 +- .../ChatMediaInputStickerGridItem.swift | 2 +- .../ChatMessageAttachedContentNode.swift | 2 + TelegramUI/ChatMessageBubbleItemNode.swift | 45 +- TelegramUI/ChatMessageItemView.swift | 400 +++++++++++++++++- .../ChatMessagePollBubbleContentNode.swift | 8 +- .../ChatMessageSelectionInputPanelNode.swift | 45 +- .../ChatMessageTextBubbleContentNode.swift | 11 + .../ChatRecentActionsControllerNode.swift | 4 +- TelegramUI/ChatSearchInputPanelNode.swift | 2 +- ...xtInputAudioRecordingCancelIndicator.swift | 2 +- TelegramUI/ChatTextInputPanelNode.swift | 4 +- TelegramUI/DeviceContactInfoController.swift | 2 +- TelegramUI/EditAccessoryPanelNode.swift | 19 +- TelegramUI/ForwardAccessoryPanelNode.swift | 21 +- TelegramUI/GroupInfoController.swift | 2 +- TelegramUI/InstantPageControllerNode.swift | 10 +- .../InstantPageReferenceControllerNode.swift | 4 +- TelegramUI/ManagedAudioSession.swift | 4 +- .../MediaNavigationAccessoryHeaderNode.swift | 19 +- TelegramUI/OverlayPlayerControlsNode.swift | 4 +- TelegramUI/ReplyAccessoryPanelNode.swift | 23 +- TelegramUI/SearchBarNode.swift | 2 +- TelegramUI/SearchBarPlaceholderNode.swift | 2 +- TelegramUI/SecureIdAuthFormFieldNode.swift | 4 +- TelegramUI/SettingsController.swift | 4 +- TelegramUI/StickerPackPreviewGridItem.swift | 2 +- TelegramUI/ThemeAccentColorActionSheet.swift | 2 +- TelegramUI/UserInfoController.swift | 4 +- 34 files changed, 589 insertions(+), 82 deletions(-) diff --git a/TelegramUI/AuthorizationSequenceAwaitingAccountResetControllerNode.swift b/TelegramUI/AuthorizationSequenceAwaitingAccountResetControllerNode.swift index 4c2df44a24..cc9e3634d5 100644 --- a/TelegramUI/AuthorizationSequenceAwaitingAccountResetControllerNode.swift +++ b/TelegramUI/AuthorizationSequenceAwaitingAccountResetControllerNode.swift @@ -54,12 +54,12 @@ final class AuthorizationSequenceAwaitingAccountResetControllerNode: ASDisplayNo self.noticeNode.displaysAsynchronously = false self.timerTitleNode = ASTextNode() - self.timerTitleNode.isLayerBacked = true + self.timerTitleNode.isUserInteractionEnabled = false self.timerTitleNode.displaysAsynchronously = false self.timerTitleNode.attributedText = NSAttributedString(string: strings.Login_ResetAccountProtected_TimerTitle, font: Font.regular(16.0), textColor: self.theme.list.itemPrimaryTextColor) self.timerValueNode = ASTextNode() - self.timerValueNode.isLayerBacked = true + self.timerValueNode.isUserInteractionEnabled = false self.timerValueNode.displaysAsynchronously = false self.resetNode = HighlightableButtonNode() diff --git a/TelegramUI/ChannelInfoController.swift b/TelegramUI/ChannelInfoController.swift index 993cc3f313..c9d1ac23e1 100644 --- a/TelegramUI/ChannelInfoController.swift +++ b/TelegramUI/ChannelInfoController.swift @@ -1024,7 +1024,7 @@ public func channelInfoController(context: AccountContext, peerId: PeerId) -> Vi return false }) if let resultItemNode = resultItemNode { - let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(presentationData.strings.Conversation_ContextMenuCopy), action: { + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: presentationData.strings.Conversation_ContextMenuCopy), action: { UIPasteboard.general.string = text })]) strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in diff --git a/TelegramUI/ChannelVisibilityController.swift b/TelegramUI/ChannelVisibilityController.swift index 13c91fa264..c33093761d 100644 --- a/TelegramUI/ChannelVisibilityController.swift +++ b/TelegramUI/ChannelVisibilityController.swift @@ -1221,7 +1221,7 @@ public func channelVisibilityController(context: AccountContext, peerId: PeerId, }) if let resultItemNode = resultItemNode { let presentationData = context.sharedContext.currentPresentationData.with { $0 } - let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(presentationData.strings.Conversation_ContextMenuCopyLink), action: { + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuCopyLink, accessibilityLabel: presentationData.strings.Conversation_ContextMenuCopyLink), action: { UIPasteboard.general.string = text })]) strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in diff --git a/TelegramUI/ChatController.swift b/TelegramUI/ChatController.swift index 3ffa612b48..6706fde6ed 100644 --- a/TelegramUI/ChatController.swift +++ b/TelegramUI/ChatController.swift @@ -1243,6 +1243,7 @@ public final class ChatController: TelegramController, KeyShortcutResponder, Gal } chatInfoButtonItem.target = self chatInfoButtonItem.action = #selector(self.rightNavigationButtonAction) + chatInfoButtonItem.accessibilityLabel = "Info" self.chatInfoNavigationButton = ChatNavigationButton(action: .openChatInfo, buttonItem: chatInfoButtonItem) self.updateChatPresentationInterfaceState(animated: false, interactive: false, { state in diff --git a/TelegramUI/ChatControllerNode.swift b/TelegramUI/ChatControllerNode.swift index 39f8420b7b..b9081e079e 100644 --- a/TelegramUI/ChatControllerNode.swift +++ b/TelegramUI/ChatControllerNode.swift @@ -211,6 +211,7 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate { self.inputPanelBackgroundSeparatorNode.isLayerBacked = true self.navigateButtons = ChatHistoryNavigationButtons(theme: self.chatPresentationInterfaceState.theme) + self.navigateButtons.accessibilityElementsHidden = true super.init() diff --git a/TelegramUI/ChatInterfaceStateContextMenus.swift b/TelegramUI/ChatInterfaceStateContextMenus.swift index ce2986c71f..04564788d1 100644 --- a/TelegramUI/ChatInterfaceStateContextMenus.swift +++ b/TelegramUI/ChatInterfaceStateContextMenus.swift @@ -338,7 +338,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: } if data.canReply { - actions.append(.context(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_ContextMenuReply), action: { + actions.append(.context(ContextMenuAction(content: .text(title: chatPresentationInterfaceState.strings.Conversation_ContextMenuReply, accessibilityLabel: chatPresentationInterfaceState.strings.Conversation_ContextMenuReply), action: { interfaceInteraction.setupReplyMessage(messages[0].id) }))) } @@ -358,7 +358,7 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: if !messages[0].text.isEmpty || resourceAvailable { let message = messages[0] - actions.append(.context(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy), action: { + actions.append(.context(ContextMenuAction(content: .text(title: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy, accessibilityLabel: chatPresentationInterfaceState.strings.Conversation_ContextMenuCopy), action: { if resourceAvailable { for media in message.media { if let image = media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) { @@ -489,12 +489,12 @@ func contextMenuForChatPresentationIntefaceState(chatPresentationInterfaceState: } } if data.canSelect { - actions.append(.context(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_ContextMenuMore), action: { + actions.append(.context(ContextMenuAction(content: .text(title: chatPresentationInterfaceState.strings.Conversation_ContextMenuMore, accessibilityLabel: chatPresentationInterfaceState.strings.Conversation_ContextMenuMore.replacingOccurrences(of: "...", with: "")), action: { interfaceInteraction.beginMessageSelection(selectAll ? messages.map { $0.id } : [message.id]) }))) } if !data.messageActions.options.intersection([.deleteLocally, .deleteGlobally]).isEmpty && isAction { - actions.append(.context(ContextMenuAction(content: .text(chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete), action: { + actions.append(.context(ContextMenuAction(content: .text(title: chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete, accessibilityLabel: chatPresentationInterfaceState.strings.Conversation_ContextMenuDelete), action: { interfaceInteraction.deleteMessages(messages) }))) } diff --git a/TelegramUI/ChatMediaInputStickerGridItem.swift b/TelegramUI/ChatMediaInputStickerGridItem.swift index a5d54411c0..f3d6df8816 100644 --- a/TelegramUI/ChatMediaInputStickerGridItem.swift +++ b/TelegramUI/ChatMediaInputStickerGridItem.swift @@ -48,7 +48,7 @@ final class ChatMediaInputStickerGridSectionNode: ASDisplayNode { init(collectionInfo: StickerPackCollectionInfo?, canManagePeerSpecificPack: Bool?, theme: PresentationTheme, interaction: ChatMediaInputNodeInteraction) { self.interaction = interaction self.titleNode = ASTextNode() - self.titleNode.isLayerBacked = true + self.titleNode.isUserInteractionEnabled = false if collectionInfo?.id.namespace == ChatMediaInputPanelAuxiliaryNamespace.peerSpecific.rawValue, let canManage = canManagePeerSpecificPack, canManage { let setupNode = HighlightableButtonNode() diff --git a/TelegramUI/ChatMessageAttachedContentNode.swift b/TelegramUI/ChatMessageAttachedContentNode.swift index eefcbdbee4..384d48a730 100644 --- a/TelegramUI/ChatMessageAttachedContentNode.swift +++ b/TelegramUI/ChatMessageAttachedContentNode.swift @@ -158,6 +158,8 @@ final class ChatMessageAttachedContentButtonNode: HighlightTrackingButtonNode { return (textSize.size.width + labelInset * 2.0, { refinedWidth in return (CGSize(width: refinedWidth, height: 33.0), { + targetNode.accessibilityLabel = title + if let updatedRegularImage = updatedRegularImage { targetNode.regularImage = updatedRegularImage if !targetNode.textNode.isHidden { diff --git a/TelegramUI/ChatMessageBubbleItemNode.swift b/TelegramUI/ChatMessageBubbleItemNode.swift index 5fdd0a212c..1dac09f2d8 100644 --- a/TelegramUI/ChatMessageBubbleItemNode.swift +++ b/TelegramUI/ChatMessageBubbleItemNode.swift @@ -172,6 +172,16 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { self.addSubnode(self.backgroundNode) self.addSubnode(self.messageAccessibilityArea) + + self.messageAccessibilityArea.activate = { [weak self] in + guard let strongSelf = self, let accessibilityData = strongSelf.accessibilityData else { + return false + } + if let singleUrl = accessibilityData.singleUrl { + strongSelf.item?.controllerInteraction.openUrl(singleUrl, false, false) + } + return false + } } required init?(coder aDecoder: NSCoder) { @@ -1471,11 +1481,44 @@ class ChatMessageBubbleItemNode: ChatMessageItemView { } } - private func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { + 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 { + var subFrame = self.backgroundNode.frame + if case .group = item.content { + for contentNode in self.contentNodes { + if contentNode.item?.message.stableId == item.message.stableId { + subFrame = contentNode.frame.insetBy(dx: 0.0, dy: -4.0) + break + } + } + } + item.controllerInteraction.openMessageContextMenu(item.message, false, self, subFrame) + } + } + } } private func addContentNode(node: ChatMessageBubbleContentNode) { diff --git a/TelegramUI/ChatMessageItemView.swift b/TelegramUI/ChatMessageItemView.swift index 6593cf0f6e..00bc71f8b2 100644 --- a/TelegramUI/ChatMessageItemView.swift +++ b/TelegramUI/ChatMessageItemView.swift @@ -114,17 +114,43 @@ private let musicDurationFormatter: DateComponentsFormatter = { return formatter }() +private let fileSizeFormatter: ByteCountFormatter = { + let formatter = ByteCountFormatter() + formatter.allowsNonnumericFormatting = true + return formatter +}() + +enum ChatMessageAccessibilityCustomActionType { + case reply + case options +} + +final class ChatMessageAccessibilityCustomAction: UIAccessibilityCustomAction { + let action: ChatMessageAccessibilityCustomActionType + + init(name: String, target: Any?, selector: Selector, action: ChatMessageAccessibilityCustomActionType) { + self.action = action + + super.init(name: name, target: target, selector: selector) + } +} + final class ChatMessageAccessibilityData { let label: String? let value: String? let hint: String? let traits: UIAccessibilityTraits + let customActions: [ChatMessageAccessibilityCustomAction]? + let singleUrl: String? init(item: ChatMessageItem, isSelected: Bool?) { - var label: String + var label: String = "" let value: String var hint: String? var traits: UIAccessibilityTraits = 0 + var singleUrl: String? + + var customActions: [ChatMessageAccessibilityCustomAction] = [] let isIncoming = item.message.effectivelyIncoming(item.context.account.peerId) var announceIncomingAuthors = false @@ -136,17 +162,7 @@ final class ChatMessageAccessibilityData { } } - var authorName: String? - if let author = item.message.author { - authorName = author.displayTitle - if isIncoming { - label = author.displayTitle - } else { - label = "Your message" - } - } else { - label = "Message" - } + let authorName = item.message.author?.displayTitle 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) @@ -154,10 +170,26 @@ final class ChatMessageAccessibilityData { var text = messageText loop: for media in item.message.media { - if let file = media as? TelegramMediaFile { + if let _ = media as? TelegramMediaImage { + if isIncoming { + if announceIncomingAuthors, let authorName = authorName { + label = "Photo, from: \(authorName)" + } else { + label = "Photo" + } + } else { + label = "Your photo" + } + text = "" + if !item.message.text.isEmpty { + text.append("\nCaption: \(item.message.text)") + } + } 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 = "Double tap to play" } @@ -176,8 +208,12 @@ final class ChatMessageAccessibilityData { text = "Duration: \(durationString)" } else { let durationString = musicDurationFormatter.string(from: Double(audio.duration)) ?? "" - if announceIncomingAuthors, let authorName = authorName { - label = "Music file, from: \(authorName)" + if isIncoming { + if announceIncomingAuthors, let authorName = authorName { + label = "Music file, from: \(authorName)" + } else { + label = "Music file" + } } else { label = "Your music file" } @@ -185,11 +221,233 @@ final class ChatMessageAccessibilityData { let title = audio.title ?? "Unknown" text = "\(title), by \(performer). Duration: \(durationString)" } + case let .Video(video): + isSpecialFile = true + if isSelected == nil { + hint = "Double tap to play" + } + traits |= UIAccessibilityTraitStartsMediaSession + let durationString = voiceMessageDurationFormatter.string(from: Double(video.duration)) ?? "" + if video.flags.contains(.instantRoundVideo) { + if isIncoming { + if announceIncomingAuthors, let authorName = authorName { + label = "Video message, from: \(authorName)" + } else { + label = "Video message" + } + } else { + label = "Your video message" + } + } else { + if isIncoming { + if announceIncomingAuthors, let authorName = authorName { + label = "Video, from: \(authorName)" + } else { + label = "Video" + } + } else { + label = "Your video" + } + } + text = "Duration: \(durationString)" default: break } } + if !isSpecialFile { + if isSelected == nil { + hint = "Double tap to open" + } + let sizeString = fileSizeFormatter.string(fromByteCount: Int64(file.size ?? 0)) + if isIncoming { + if announceIncomingAuthors, let authorName = authorName { + label = "File, from: \(authorName)" + } else { + label = "File" + } + } else { + label = "Your file" + } + text = "\(file.fileName ?? ""). Size: \(sizeString)" + } + if !item.message.text.isEmpty { + text.append("\nCaption: \(item.message.text)") + } break loop + } else if let webpage = media as? TelegramMediaWebpage, case let .Loaded(content) = webpage.content { + var contentText = "Page preview. " + if let title = content.title, !title.isEmpty { + contentText.append("Title: \(title). ") + } + 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 = "Shared contact, from: \(authorName)" + } else { + label = "Shared contact" + } + } else { + label = "Your shared contact" + } + 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 + } + + for email in contactData.emailAddresses { + if !emailAddressesString.isEmpty { + emailAddressesString.append(", ") + } + 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("\(phoneNumberCount) phone numbers: ") + } else { + text.append("Phone number: ") + } + text.append("\(phoneNumbersString). ") + } + if !emailAddressesString.isEmpty { + if emailAddressCount > 1 { + text.append("\(emailAddressCount) email addresses: ") + } else { + text.append("Email: ") + } + text.append("\(emailAddressesString). ") + } + if !organizationString.isEmpty { + text.append("Organization: \(organizationString).") + } + } else if let poll = media as? TelegramMediaPoll { + if isIncoming { + if announceIncomingAuthors, let authorName = authorName { + label = "Anonymous poll, from: \(authorName)" + } else { + label = "Anonymous poll" + } + } else { + label = "Your anonymous poll" + } + + 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 + } + } + } + } + } + + 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 = "Title: \(poll.text). " + + text.append("\(poll.options.count) options: ") + 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(", selected") + } + + if let _ = optionVoterCount[i] { + if maxOptionVoterCount != 0 && totalVoterCount != 0 { + optionsText.append(", \(optionVoterCounts[i])%") + } + } + } + text.append("\(optionsText). ") + if totalVoterCount != 0 { + if totalVoterCount == 1 { + text.append("1 vote. ") + } else { + text.append("\(totalVoterCount) votes. ") + } + } else { + text.append("No votes. ") + } + if poll.isClosed { + text.append("Final results. ") + } } } @@ -204,7 +462,7 @@ final class ChatMessageAccessibilityData { result += "\(text)" - let dateString = DateFormatter.localizedString(from: Date(timeIntervalSince1970: Double(item.message.timestamp)), dateStyle: DateFormatter.Style.medium, timeStyle: DateFormatter.Style.short) + let dateString = DateFormatter.localizedString(from: Date(timeIntervalSince1970: Double(item.message.timestamp)), dateStyle: .medium, timeStyle: .short) result += "\n\(dateString)" if !isIncoming && item.read { @@ -219,10 +477,115 @@ final class ChatMessageAccessibilityData { value = "" } + if label.isEmpty { + if let author = item.message.author { + if isIncoming { + label = author.displayTitle + } else { + label = "Your message" + } + } else { + label = "Message" + } + } + + for attribute in item.message.attributes { + if let attribute = attribute as? TextEntitiesMessageAttribute { + var hasUrls = false + loop: for entity in attribute.entities { + switch entity.type { + case .Url: + if hasUrls { + singleUrl = nil + break loop + } else { + if let range = Range(NSRange(location: entity.range.lowerBound, length: entity.range.count), in: item.message.text) { + singleUrl = String(item.message.text[range]) + hasUrls = true + } + } + case let .TextUrl(url): + if hasUrls { + singleUrl = nil + break loop + } else { + singleUrl = url + hasUrls = true + } + default: + break + } + } + } else if let attribute = attribute as? ReplyMessageAttribute, let replyMessage = item.message.associatedMessages[attribute.messageId] { + let replyLabel: String + if replyMessage.flags.contains(.Incoming) { + if let author = replyMessage.author { + replyLabel = "Reply to message from \(author.displayTitle)" + } else { + replyLabel = "Reply to message" + } + } else { + replyLabel = "Reply to your message" + } + label = "\(replyLabel) . \(label)" + } + } + + if hint == nil && singleUrl != nil { + hint = "Double tap to open link" + } + + if let forwardInfo = item.message.forwardInfo { + let forwardLabel: String + if let author = forwardInfo.author, author.id == item.context.account.peerId { + forwardLabel = "Forwarded from you" + } else { + let peerString: String + if let peer = forwardInfo.author { + if let authorName = forwardInfo.authorSignature { + peerString = "\(peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder)) (\(authorName))" + } else { + peerString = peer.displayTitle(strings: item.presentationData.strings, displayOrder: item.presentationData.nameDisplayOrder) + } + } else if let authorName = forwardInfo.authorSignature { + peerString = authorName + } else { + peerString = "" + } + forwardLabel = "Forwarded from \(peerString)" + } + label = "\(forwardLabel). \(label)" + } + + if isSelected == nil { + var canReply = item.controllerInteraction.canSetupReply(item.message) + for media in item.content.firstMessage.media { + if let _ = media as? TelegramMediaExpiredContent { + canReply = false + } + else if let media = media as? TelegramMediaAction { + if case .phoneCall(_, _, _) = media.action { + } else { + canReply = false + } + } + } + + if canReply { + customActions.append(ChatMessageAccessibilityCustomAction(name: "Reply", target: nil, selector: #selector(self.noop), action: .reply)) + } + customActions.append(ChatMessageAccessibilityCustomAction(name: "Open message menu", target: nil, selector: #selector(self.noop), action: .options)) + } + self.label = label self.value = value self.hint = hint self.traits = traits + self.customActions = customActions.isEmpty ? nil : customActions + self.singleUrl = singleUrl + } + + @objc private func noop() { } } @@ -230,6 +593,7 @@ public class ChatMessageItemView: ListViewItemNode { let layoutConstants = defaultChatMessageItemLayoutConstants var item: ChatMessageItem? + var accessibilityData: ChatMessageAccessibilityData? public required convenience init() { self.init(layerBacked: false) @@ -255,6 +619,10 @@ public class ChatMessageItemView: ListViewItemNode { self.item = item } + func updateAccessibilityData(_ accessibilityData: ChatMessageAccessibilityData) { + self.accessibilityData = accessibilityData + } + override public func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) { if let item = item as? ChatMessageItem { let doLayout = self.asyncLayout() diff --git a/TelegramUI/ChatMessagePollBubbleContentNode.swift b/TelegramUI/ChatMessagePollBubbleContentNode.swift index 7414a3a77b..86a2a1b3ae 100644 --- a/TelegramUI/ChatMessagePollBubbleContentNode.swift +++ b/TelegramUI/ChatMessagePollBubbleContentNode.swift @@ -4,7 +4,7 @@ import Display import TelegramCore import Postbox -private struct PercentCounterItem: Comparable { +struct PercentCounterItem: Comparable { var index: Int = 0 var percent: Int = 0 var remainder: Int = 0 @@ -20,7 +20,7 @@ private struct PercentCounterItem: Comparable { } -private func adjustPercentCount(_ items: [PercentCounterItem], left: Int) -> [PercentCounterItem] { +func adjustPercentCount(_ items: [PercentCounterItem], left: Int) -> [PercentCounterItem] { var left = left var items = items.sorted(by: <) var i:Int = 0 @@ -47,7 +47,7 @@ private func adjustPercentCount(_ items: [PercentCounterItem], left: Int) -> [Pe return items } -private func countNicePercent(votes: [Int], total: Int) -> [Int] { +func countNicePercent(votes: [Int], total: Int) -> [Int] { var result:[Int] = [] var items:[PercentCounterItem] = [] for _ in votes { @@ -401,6 +401,8 @@ private final class ChatMessagePollOptionNode: ASDisplayNode { node.highlightedBackgroundNode.backgroundColor = (incoming ? presentationData.theme.theme.chat.bubble.incomingAccentTextColor : presentationData.theme.theme.chat.bubble.outgoingAccentTextColor).withAlphaComponent(0.15) + node.buttonNode.accessibilityLabel = option.text + let titleNode = titleApply() if node.titleNode !== titleNode { node.titleNode = titleNode diff --git a/TelegramUI/ChatMessageSelectionInputPanelNode.swift b/TelegramUI/ChatMessageSelectionInputPanelNode.swift index 48e3d2a8dc..548c83ec1c 100644 --- a/TelegramUI/ChatMessageSelectionInputPanelNode.swift +++ b/TelegramUI/ChatMessageSelectionInputPanelNode.swift @@ -6,10 +6,10 @@ import TelegramCore import SwiftSignalKit final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { - private let deleteButton: UIButton - private let reportButton: UIButton - private let forwardButton: UIButton - private let shareButton: UIButton + private let deleteButton: HighlightableButtonNode + private let reportButton: HighlightableButtonNode + private let forwardButton: HighlightableButtonNode + private let shareButton: HighlightableButtonNode private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, metrics: LayoutMetrics)? private var presentationInterfaceState: ChatPresentationInterfaceState? @@ -48,13 +48,24 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { init(theme: PresentationTheme) { self.theme = theme - self.deleteButton = UIButton() + self.deleteButton = HighlightableButtonNode() self.deleteButton.isEnabled = false - self.reportButton = UIButton() + self.deleteButton.isAccessibilityElement = true + self.deleteButton.accessibilityLabel = "Delete" + + self.reportButton = HighlightableButtonNode() self.reportButton.isEnabled = false - self.forwardButton = UIButton() - self.shareButton = UIButton() + self.reportButton.isAccessibilityElement = true + self.reportButton.accessibilityLabel = "Report" + + self.forwardButton = HighlightableButtonNode() + self.forwardButton.isAccessibilityElement = true + self.forwardButton.accessibilityLabel = "Forward" + + self.shareButton = HighlightableButtonNode() self.shareButton.isEnabled = false + self.shareButton.isAccessibilityElement = true + self.shareButton.accessibilityLabel = "Share" self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionThrash"), color: theme.chat.inputPanel.panelControlAccentColor), for: [.normal]) self.deleteButton.setImage(generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Accessory Panels/MessageSelectionThrash"), color: theme.chat.inputPanel.panelControlDisabledColor), for: [.disabled]) @@ -67,17 +78,17 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { super.init() - self.view.addSubview(self.deleteButton) - self.view.addSubview(self.reportButton) - self.view.addSubview(self.forwardButton) - self.view.addSubview(self.shareButton) + self.addSubnode(self.deleteButton) + self.addSubnode(self.reportButton) + self.addSubnode(self.forwardButton) + self.addSubnode(self.shareButton) self.forwardButton.isEnabled = false - self.deleteButton.addTarget(self, action: #selector(self.deleteButtonPressed), for: [.touchUpInside]) - self.reportButton.addTarget(self, action: #selector(self.reportButtonPressed), for: [.touchUpInside]) - self.forwardButton.addTarget(self, action: #selector(self.forwardButtonPressed), for: [.touchUpInside]) - self.shareButton.addTarget(self, action: #selector(self.shareButtonPressed), for: [.touchUpInside]) + self.deleteButton.addTarget(self, action: #selector(self.deleteButtonPressed), forControlEvents: .touchUpInside) + self.reportButton.addTarget(self, action: #selector(self.reportButtonPressed), forControlEvents: .touchUpInside) + self.forwardButton.addTarget(self, action: #selector(self.forwardButtonPressed), forControlEvents: .touchUpInside) + self.shareButton.addTarget(self, action: #selector(self.shareButtonPressed), forControlEvents: .touchUpInside) } deinit { @@ -155,7 +166,7 @@ final class ChatMessageSelectionInputPanelNode: ChatInputPanelNode { self.forwardButton.frame = CGRect(origin: CGPoint(x: width - rightInset - 57.0, y: 0.0), size: CGSize(width: 57.0, height: panelHeight)) self.shareButton.frame = CGRect(origin: CGPoint(x: floor((width - rightInset - 57.0) / 2.0), y: 0.0), size: CGSize(width: 57.0, height: panelHeight)) } else if !self.deleteButton.isHidden { - let buttons: [UIButton] = [ + let buttons: [HighlightableButtonNode] = [ self.deleteButton, self.reportButton, self.shareButton, diff --git a/TelegramUI/ChatMessageTextBubbleContentNode.swift b/TelegramUI/ChatMessageTextBubbleContentNode.swift index 18531dbd6d..cbd9989555 100644 --- a/TelegramUI/ChatMessageTextBubbleContentNode.swift +++ b/TelegramUI/ChatMessageTextBubbleContentNode.swift @@ -32,6 +32,7 @@ private final class CachedChatMessageText { class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { private let textNode: TextNode + private let textAccessibilityOverlayNode: TextAccessibilityOverlayNode private let statusNode: ChatMessageDateAndStatusNode private var linkHighlightingNode: LinkHighlightingNode? @@ -41,8 +42,11 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { required init() { self.textNode = TextNode() + self.statusNode = ChatMessageDateAndStatusNode() + self.textAccessibilityOverlayNode = TextAccessibilityOverlayNode() + super.init() self.textNode.isUserInteractionEnabled = false @@ -50,6 +54,11 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { self.textNode.contentsScale = UIScreenScale self.textNode.displaysAsynchronously = true self.addSubnode(self.textNode) + self.addSubnode(self.textAccessibilityOverlayNode) + + self.textAccessibilityOverlayNode.openUrl = { [weak self] url in + self?.item?.controllerInteraction.openUrl(url, false, false) + } } required init?(coder aDecoder: NSCoder) { @@ -280,6 +289,8 @@ class ChatMessageTextBubbleContentNode: ChatMessageBubbleContentNode { } strongSelf.textNode.frame = textFrame + strongSelf.textAccessibilityOverlayNode.frame = textFrame + strongSelf.textAccessibilityOverlayNode.cachedLayout = textLayout } }) }) diff --git a/TelegramUI/ChatRecentActionsControllerNode.swift b/TelegramUI/ChatRecentActionsControllerNode.swift index f3673aa561..dbe76c01cf 100644 --- a/TelegramUI/ChatRecentActionsControllerNode.swift +++ b/TelegramUI/ChatRecentActionsControllerNode.swift @@ -658,7 +658,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { private func openMessageContextMenu(message: Message, selectAll: Bool, node: ASDisplayNode, frame: CGRect) { var actions: [ContextMenuAction] = [] if !message.text.isEmpty { - actions.append(ContextMenuAction(content: .text(self.presentationData.strings.Conversation_ContextMenuCopy), action: { + actions.append(ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { UIPasteboard.general.string = message.text })) } @@ -686,7 +686,7 @@ final class ChatRecentActionsControllerNode: ViewControllerTracingNode { } if canBan { - actions.append(ContextMenuAction(content: .text(self.presentationData.strings.Conversation_ContextMenuBan), action: { [weak self] in + actions.append(ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuBan, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuBan), action: { [weak self] in if let strongSelf = self { strongSelf.banDisposables.set((fetchChannelParticipant(account: strongSelf.context.account, peerId: strongSelf.peer.id, participantId: author.id) |> deliverOnMainQueue).start(next: { participant in diff --git a/TelegramUI/ChatSearchInputPanelNode.swift b/TelegramUI/ChatSearchInputPanelNode.swift index 1d855a4075..f8e913cc25 100644 --- a/TelegramUI/ChatSearchInputPanelNode.swift +++ b/TelegramUI/ChatSearchInputPanelNode.swift @@ -47,7 +47,7 @@ final class ChatSearchInputPanelNode: ChatInputPanelNode { self.calendarButton = HighlightableButtonNode() self.membersButton = HighlightableButtonNode() self.resultsLabel = TextNode() - self.resultsLabel.isLayerBacked = true + self.resultsLabel.isUserInteractionEnabled = false self.resultsLabel.displaysAsynchronously = false self.activityIndicator = ActivityIndicator(type: .navigationAccent(theme)) self.activityIndicator.isHidden = true diff --git a/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift b/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift index af885e0ef6..90c96f9fe4 100644 --- a/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift +++ b/TelegramUI/ChatTextInputAudioRecordingCancelIndicator.swift @@ -24,7 +24,7 @@ final class ChatTextInputAudioRecordingCancelIndicator: ASDisplayNode { self.labelNode = TextNode() self.labelNode.displaysAsynchronously = false - self.labelNode.isLayerBacked = true + self.labelNode.isUserInteractionEnabled = false self.cancelButton = HighlightableButtonNode() self.cancelButton.setTitle(strings.Common_Cancel, with: cancelFont, with: theme.chat.inputPanel.panelControlAccentColor, for: []) diff --git a/TelegramUI/ChatTextInputPanelNode.swift b/TelegramUI/ChatTextInputPanelNode.swift index dc88bb0d2a..1cd321dc97 100644 --- a/TelegramUI/ChatTextInputPanelNode.swift +++ b/TelegramUI/ChatTextInputPanelNode.swift @@ -324,7 +324,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { self.textInputBackgroundView = UIImageView() self.textPlaceholderNode = ImmediateTextNode() self.textPlaceholderNode.maximumNumberOfLines = 1 - self.textPlaceholderNode.isLayerBacked = true + self.textPlaceholderNode.isUserInteractionEnabled = false self.attachmentButton = HighlightableButtonNode() self.attachmentButton.accessibilityLabel = "Send media" self.attachmentButton.isAccessibilityElement = true @@ -1032,7 +1032,7 @@ class ChatTextInputPanelNode: ChatInputPanelNode, ASEditableTextNodeDelegate { if self.contextPlaceholderNode !== contextPlaceholderNode { contextPlaceholderNode.displaysAsynchronously = false - contextPlaceholderNode.isLayerBacked = true + contextPlaceholderNode.isUserInteractionEnabled = false self.contextPlaceholderNode = contextPlaceholderNode self.insertSubnode(contextPlaceholderNode, aboveSubnode: self.textPlaceholderNode) } diff --git a/TelegramUI/DeviceContactInfoController.swift b/TelegramUI/DeviceContactInfoController.swift index 5bf097edc6..412df6c9c9 100644 --- a/TelegramUI/DeviceContactInfoController.swift +++ b/TelegramUI/DeviceContactInfoController.swift @@ -1112,7 +1112,7 @@ public func deviceContactInfoController(context: AccountContext, subject: Device return false }) if let resultItemNode = resultItemNode { - let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(presentationData.strings.Conversation_ContextMenuCopy), action: { + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: presentationData.strings.Conversation_ContextMenuCopy), action: { UIPasteboard.general.string = value })]) strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in diff --git a/TelegramUI/EditAccessoryPanelNode.swift b/TelegramUI/EditAccessoryPanelNode.swift index 31bb324ff6..f74d4ca135 100644 --- a/TelegramUI/EditAccessoryPanelNode.swift +++ b/TelegramUI/EditAccessoryPanelNode.swift @@ -14,6 +14,8 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { let textNode: ImmediateTextNode let imageNode: TransformImageNode + private let actionArea: AccessibilityAreaNode + private let activityIndicator: ActivityIndicator private let statusNode: RadialStatusNode private let tapNode: ASDisplayNode @@ -61,6 +63,7 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { self.nameDisplayOrder = nameDisplayOrder self.closeButton = ASButtonNode() + self.closeButton.accessibilityLabel = "Discard" self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: []) self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) self.closeButton.displaysAsynchronously = false @@ -91,6 +94,8 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { self.tapNode = ASDisplayNode() + self.actionArea = AccessibilityAreaNode() + super.init() self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) @@ -103,6 +108,7 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { self.addSubnode(self.activityIndicator) self.addSubnode(self.statusNode) self.addSubnode(self.tapNode) + self.addSubnode(self.actionArea) self.messageDisposable.set((context.account.postbox.messageAtId(messageId) |> deliverOnMainQueue).start(next: { [weak self] message in self?.updateMessage(message) @@ -215,9 +221,13 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { canEditMedia = false } - self.titleNode.attributedText = NSAttributedString(string: canEditMedia ? self.strings.Conversation_EditingCaptionPanelTitle : self.strings.Conversation_EditingMessagePanelTitle, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) + let titleString = canEditMedia ? self.strings.Conversation_EditingCaptionPanelTitle : self.strings.Conversation_EditingMessagePanelTitle + self.titleNode.attributedText = NSAttributedString(string: titleString, font: Font.medium(15.0), textColor: self.theme.chat.inputPanel.panelControlAccentColor) self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: isMedia ? self.theme.chat.inputPanel.secondaryTextColor : self.theme.chat.inputPanel.primaryTextColor) + let headerString: String = titleString + self.actionArea.accessibilityLabel = "\(headerString).\n\(text)" + if let applyImage = applyImage { applyImage() self.imageNode.isHidden = false @@ -297,8 +307,11 @@ final class EditAccessoryPanelNode: AccessoryPanelNode { self.activityIndicator.frame = CGRect(origin: CGPoint(x: 18.0, y: 15.0), size: indicatorSize) self.statusNode.frame = CGRect(origin: CGPoint(x: 18.0, y: 15.0), size: indicatorSize).insetBy(dx: -2.0, dy: -2.0) - let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) - self.closeButton.frame = CGRect(origin: CGPoint(x: bounds.size.width - rightInset - closeButtonSize.width, y: 19.0), size: closeButtonSize) + let closeButtonSize = CGSize(width: 44.0, height: bounds.height) + let closeButtonFrame = CGRect(origin: CGPoint(x: bounds.width - rightInset - closeButtonSize.width + 12.0, y: 2.0), size: closeButtonSize) + self.closeButton.frame = closeButtonFrame + + self.actionArea.frame = CGRect(origin: CGPoint(x: leftInset, y: 2.0), size: CGSize(width: closeButtonFrame.minX - leftInset, height: bounds.height)) self.lineNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 10.0)) diff --git a/TelegramUI/ForwardAccessoryPanelNode.swift b/TelegramUI/ForwardAccessoryPanelNode.swift index e8e25d3717..8fa1d5d96a 100644 --- a/TelegramUI/ForwardAccessoryPanelNode.swift +++ b/TelegramUI/ForwardAccessoryPanelNode.swift @@ -69,6 +69,8 @@ final class ForwardAccessoryPanelNode: AccessoryPanelNode { let titleNode: ImmediateTextNode let textNode: ImmediateTextNode + private let actionArea: AccessibilityAreaNode + var theme: PresentationTheme init(context: AccountContext, messageIds: [MessageId], theme: PresentationTheme, strings: PresentationStrings) { @@ -76,6 +78,7 @@ final class ForwardAccessoryPanelNode: AccessoryPanelNode { self.theme = theme self.closeButton = ASButtonNode() + self.closeButton.accessibilityLabel = "Discard" self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: []) self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) self.closeButton.displaysAsynchronously = false @@ -93,6 +96,8 @@ final class ForwardAccessoryPanelNode: AccessoryPanelNode { self.textNode.maximumNumberOfLines = 1 self.textNode.displaysAsynchronously = false + self.actionArea = AccessibilityAreaNode() + super.init() self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) @@ -101,6 +106,7 @@ final class ForwardAccessoryPanelNode: AccessoryPanelNode { self.addSubnode(self.lineNode) self.addSubnode(self.titleNode) self.addSubnode(self.textNode) + self.addSubnode(self.actionArea) self.messageDisposable.set((context.account.postbox.messagesAtIds(messageIds) |> deliverOnMainQueue).start(next: { [weak self] messages in @@ -127,6 +133,14 @@ final class ForwardAccessoryPanelNode: AccessoryPanelNode { strongSelf.titleNode.attributedText = NSAttributedString(string: authors, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor) strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: strongSelf.theme.chat.inputPanel.secondaryTextColor) + let headerString: String + if messages.count == 1 { + headerString = "Forward message" + } else { + headerString = "Forward messages" + } + strongSelf.actionArea.accessibilityLabel = "\(headerString). From: \(authors).\n\(text)" + strongSelf.setNeedsLayout() } })) @@ -169,8 +183,11 @@ final class ForwardAccessoryPanelNode: AccessoryPanelNode { let rightInset: CGFloat = 55.0 let textRightInset: CGFloat = 20.0 - let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) - self.closeButton.frame = CGRect(origin: CGPoint(x: bounds.size.width - rightInset - closeButtonSize.width, y: 19.0), size: closeButtonSize) + let closeButtonSize = CGSize(width: 44.0, height: bounds.height) + let closeButtonFrame = CGRect(origin: CGPoint(x: bounds.width - rightInset - closeButtonSize.width + 12.0, y: 2.0), size: closeButtonSize) + self.closeButton.frame = closeButtonFrame + + self.actionArea.frame = CGRect(origin: CGPoint(x: leftInset, y: 2.0), size: CGSize(width: closeButtonFrame.minX - leftInset, height: bounds.height)) self.lineNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 10.0)) diff --git a/TelegramUI/GroupInfoController.swift b/TelegramUI/GroupInfoController.swift index 8c2681e721..05ebba863e 100644 --- a/TelegramUI/GroupInfoController.swift +++ b/TelegramUI/GroupInfoController.swift @@ -2129,7 +2129,7 @@ public func groupInfoController(context: AccountContext, peerId originalPeerId: return false }) if let resultItemNode = resultItemNode { - let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(presentationData.strings.Conversation_ContextMenuCopy), action: { + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: presentationData.strings.Conversation_ContextMenuCopy), action: { UIPasteboard.general.string = text })]) strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in diff --git a/TelegramUI/InstantPageControllerNode.swift b/TelegramUI/InstantPageControllerNode.swift index bd55f75905..cf3124bc68 100644 --- a/TelegramUI/InstantPageControllerNode.swift +++ b/TelegramUI/InstantPageControllerNode.swift @@ -877,17 +877,17 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { } private func longPressMedia(_ media: InstantPageMedia) { - let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(self.strings.Conversation_ContextMenuCopy), action: { [weak self] in + let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in if let strongSelf = self, let image = media.media as? TelegramMediaImage { let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil) let _ = copyToPasteboard(context: strongSelf.context, postbox: strongSelf.context.account.postbox, mediaReference: .standalone(media: media)).start() } - }), ContextMenuAction(content: .text(self.strings.Conversation_LinkDialogSave), action: { [weak self] in + }), ContextMenuAction(content: .text(title: self.strings.Conversation_LinkDialogSave, accessibilityLabel: self.strings.Conversation_LinkDialogSave), action: { [weak self] in if let strongSelf = self, let image = media.media as? TelegramMediaImage { let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil) let _ = saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, mediaReference: .standalone(media: media)).start() } - }), ContextMenuAction(content: .text(self.strings.Conversation_ContextMenuShare), action: { [weak self] in + }), ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in if let strongSelf = self, let webPage = strongSelf.webPage, let image = media.media as? TelegramMediaImage { strongSelf.present(ShareController(context: strongSelf.context, subject: .image(image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.media(media: .webPage(webPage: WebpageReference(webPage), media: image), resource: $0.resource)) }))), nil) } @@ -982,9 +982,9 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate { coveringRect = coveringRect.union(rects[i]) } - let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(self.strings.Conversation_ContextMenuCopy), action: { + let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { UIPasteboard.general.string = text - }), ContextMenuAction(content: .text(self.strings.Conversation_ContextMenuShare), action: { [weak self] in + }), ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in if let strongSelf = self, let webPage = strongSelf.webPage, case let .Loaded(content) = webPage.content { strongSelf.present(ShareController(context: strongSelf.context, subject: .quote(text: text, url: content.url)), nil) } diff --git a/TelegramUI/InstantPageReferenceControllerNode.swift b/TelegramUI/InstantPageReferenceControllerNode.swift index a4ab9000e3..059478ac45 100644 --- a/TelegramUI/InstantPageReferenceControllerNode.swift +++ b/TelegramUI/InstantPageReferenceControllerNode.swift @@ -403,9 +403,9 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, UIScrollVie coveringRect = coveringRect.union(rects[i]) } - let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(self.presentationData.strings.Conversation_ContextMenuCopy), action: { + let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuCopy), action: { UIPasteboard.general.string = text - }), ContextMenuAction(content: .text(self.presentationData.strings.Conversation_ContextMenuShare), action: { [weak self] in + }), ContextMenuAction(content: .text(title: self.presentationData.strings.Conversation_ContextMenuShare, accessibilityLabel: self.presentationData.strings.Conversation_ContextMenuShare), action: { [weak self] in if let strongSelf = self, case let .Loaded(content) = strongSelf.webPage.content { strongSelf.present(ShareController(context: strongSelf.context, subject: .quote(text: text, url: content.url)), nil) } diff --git a/TelegramUI/ManagedAudioSession.swift b/TelegramUI/ManagedAudioSession.swift index 8dcdc2ac28..a86b0b9b31 100644 --- a/TelegramUI/ManagedAudioSession.swift +++ b/TelegramUI/ManagedAudioSession.swift @@ -716,7 +716,9 @@ public final class ManagedAudioSession { } if resetToBuiltin { switch type { - case .voiceCall, .playWithPossiblePortOverride, .record: + case .record(false): + try AVAudioSession.sharedInstance().overrideOutputAudioPort(.speaker) + case .voiceCall, .playWithPossiblePortOverride, .record(true): try AVAudioSession.sharedInstance().overrideOutputAudioPort(.none) if let routes = AVAudioSession.sharedInstance().availableInputs { for route in routes { diff --git a/TelegramUI/MediaNavigationAccessoryHeaderNode.swift b/TelegramUI/MediaNavigationAccessoryHeaderNode.swift index 82a70a74e5..ae44401bd0 100644 --- a/TelegramUI/MediaNavigationAccessoryHeaderNode.swift +++ b/TelegramUI/MediaNavigationAccessoryHeaderNode.swift @@ -22,6 +22,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { private let actionPauseNode: ASImageNode private let actionPlayNode: ASImageNode private let rateButton: HighlightableButtonNode + private let accessibilityAreaNode: AccessibilityAreaNode private let scrubbingNode: MediaPlayerScrubbingNode @@ -50,8 +51,14 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { switch voiceBaseRate { case .x1: self.rateButton.setImage(PresentationResourcesRootController.navigationPlayerRateInactiveIcon(self.theme), for: []) + self.rateButton.accessibilityLabel = "Playback rate" + self.rateButton.accessibilityValue = "Normal" + self.rateButton.accessibilityHint = "Double tap to change" case .x2: self.rateButton.setImage(PresentationResourcesRootController.navigationPlayerRateActiveIcon(self.theme), for: []) + self.rateButton.accessibilityLabel = "Playback rate" + self.rateButton.accessibilityValue = "Fast" + self.rateButton.accessibilityHint = "Double tap to change" } } } @@ -83,8 +90,10 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { self.subtitleNode.displaysAsynchronously = false self.closeButton = HighlightableButtonNode() + self.closeButton.accessibilityLabel = "Stop playback" self.closeButton.setImage(PresentationResourcesRootController.navigationPlayerCloseButton(self.theme), for: []) self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) + self.closeButton.contentEdgeInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 2.0) self.closeButton.displaysAsynchronously = false self.rateButton = HighlightableButtonNode() @@ -92,6 +101,8 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { self.rateButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -4.0, -8.0, -4.0) self.rateButton.displaysAsynchronously = false + self.accessibilityAreaNode = AccessibilityAreaNode() + self.actionButton = HighlightTrackingButtonNode() self.actionButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) self.actionButton.displaysAsynchronously = false @@ -126,6 +137,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { self.addSubnode(self.closeButton) self.addSubnode(self.rateButton) + self.addSubnode(self.accessibilityAreaNode) self.actionButton.addSubnode(self.actionPauseNode) self.actionButton.addSubnode(self.actionPlayNode) @@ -185,6 +197,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { } strongSelf.actionPlayNode.isHidden = !paused strongSelf.actionPauseNode.isHidden = paused + strongSelf.actionButton.accessibilityLabel = paused ? "Play" : "Pause" } } } @@ -287,6 +300,8 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: size.width - titleSideInset, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .middle, constrainedSize: CGSize(width: size.width - titleSideInset, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets())) + self.accessibilityAreaNode.accessibilityLabel = "\(titleString?.string ?? ""). \(subtitleString?.string ?? "")" + let _ = titleApply() let _ = subtitleApply() @@ -299,7 +314,7 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { transition.updateFrame(node: self.subtitleNode, frame: minimizedSubtitleFrame) let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) - transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: bounds.size.width - 18.0 - closeButtonSize.width - rightInset, y: minimizedTitleFrame.minY + 8.0), size: closeButtonSize)) + transition.updateFrame(node: self.closeButton, frame: CGRect(origin: CGPoint(x: bounds.size.width - 44.0 - rightInset, y: 0.0), size: CGSize(width: 44.0, height: minHeight))) let rateButtonSize = CGSize(width: 24.0, height: minHeight) transition.updateFrame(node: self.rateButton, frame: CGRect(origin: CGPoint(x: bounds.size.width - 18.0 - closeButtonSize.width - 18.0 - rateButtonSize.width - rightInset, y: 0.0), size: rateButtonSize)) transition.updateFrame(node: self.actionPlayNode, frame: CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: 40.0, height: 37.0))) @@ -308,6 +323,8 @@ final class MediaNavigationAccessoryHeaderNode: ASDisplayNode { transition.updateFrame(node: self.scrubbingNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 37.0 - 2.0), size: CGSize(width: size.width, height: 2.0))) transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: minHeight - UIScreenPixel), size: CGSize(width: size.width, height: UIScreenPixel))) + + self.accessibilityAreaNode.frame = CGRect(origin: CGPoint(x: self.actionButton.frame.maxX, y: 0.0), size: CGSize(width: self.rateButton.frame.minX - self.actionButton.frame.maxX, height: minHeight)) } @objc func closeButtonPressed() { diff --git a/TelegramUI/OverlayPlayerControlsNode.swift b/TelegramUI/OverlayPlayerControlsNode.swift index 0a20487685..20f6df8a97 100644 --- a/TelegramUI/OverlayPlayerControlsNode.swift +++ b/TelegramUI/OverlayPlayerControlsNode.swift @@ -119,11 +119,11 @@ final class OverlayPlayerControlsNode: ASDisplayNode { self.albumArtNode = TransformImageNode() self.titleNode = TextNode() - self.titleNode.isLayerBacked = true + self.titleNode.isUserInteractionEnabled = false self.titleNode.displaysAsynchronously = false self.descriptionNode = TextNode() - self.descriptionNode.isLayerBacked = true + self.descriptionNode.isUserInteractionEnabled = false self.descriptionNode.displaysAsynchronously = false self.shareNode = HighlightableButtonNode() diff --git a/TelegramUI/ReplyAccessoryPanelNode.swift b/TelegramUI/ReplyAccessoryPanelNode.swift index b71e61344a..4da2718d92 100644 --- a/TelegramUI/ReplyAccessoryPanelNode.swift +++ b/TelegramUI/ReplyAccessoryPanelNode.swift @@ -17,6 +17,8 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { let textNode: ImmediateTextNode let imageNode: TransformImageNode + private let actionArea: AccessibilityAreaNode + var theme: PresentationTheme init(context: AccountContext, messageId: MessageId, theme: PresentationTheme, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder) { @@ -25,6 +27,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { self.theme = theme self.closeButton = ASButtonNode() + self.closeButton.accessibilityLabel = "Discard" self.closeButton.hitTestSlop = UIEdgeInsetsMake(-8.0, -8.0, -8.0, -8.0) self.closeButton.setImage(PresentationResourcesChat.chatInputPanelCloseIconImage(theme), for: []) self.closeButton.displaysAsynchronously = false @@ -46,6 +49,8 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { self.imageNode.contentAnimations = [.subsequentUpdates] self.imageNode.isHidden = true + self.actionArea = AccessibilityAreaNode() + super.init() self.closeButton.addTarget(self, action: #selector(self.closePressed), forControlEvents: [.touchUpInside]) @@ -55,6 +60,7 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { self.addSubnode(self.titleNode) self.addSubnode(self.textNode) self.addSubnode(self.imageNode) + self.addSubnode(self.actionArea) self.messageDisposable.set((context.account.postbox.messageAtId(messageId) |> deliverOnMainQueue).start(next: { [weak self] message in @@ -144,6 +150,16 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { strongSelf.titleNode.attributedText = NSAttributedString(string: authorName, font: Font.medium(15.0), textColor: strongSelf.theme.chat.inputPanel.panelControlAccentColor) strongSelf.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: isMedia ? strongSelf.theme.chat.inputPanel.secondaryTextColor : strongSelf.theme.chat.inputPanel.primaryTextColor) + let headerString: String + if let message = message, message.flags.contains(.Incoming), let author = message.author { + headerString = "Reply to message. From: \(author.displayTitle)" + } else if let message = message, !message.flags.contains(.Incoming) { + headerString = "Reply to your message" + } else { + headerString = "Reply to message" + } + strongSelf.actionArea.accessibilityLabel = "\(headerString).\n\(text)" + if let applyImage = applyImage { applyImage() strongSelf.imageNode.isHidden = false @@ -203,8 +219,11 @@ final class ReplyAccessoryPanelNode: AccessoryPanelNode { let rightInset: CGFloat = 55.0 let textRightInset: CGFloat = 20.0 - let closeButtonSize = self.closeButton.measure(CGSize(width: 100.0, height: 100.0)) - self.closeButton.frame = CGRect(origin: CGPoint(x: bounds.size.width - rightInset - closeButtonSize.width, y: 19.0), size: closeButtonSize) + let closeButtonSize = CGSize(width: 44.0, height: bounds.height) + let closeButtonFrame = CGRect(origin: CGPoint(x: bounds.width - rightInset - closeButtonSize.width + 12.0, y: 2.0), size: closeButtonSize) + self.closeButton.frame = closeButtonFrame + + self.actionArea.frame = CGRect(origin: CGPoint(x: leftInset, y: 2.0), size: CGSize(width: closeButtonFrame.minX - leftInset, height: bounds.height)) self.lineNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 8.0), size: CGSize(width: 2.0, height: bounds.size.height - 10.0)) diff --git a/TelegramUI/SearchBarNode.swift b/TelegramUI/SearchBarNode.swift index 24d4547bb1..e94ee0a261 100644 --- a/TelegramUI/SearchBarNode.swift +++ b/TelegramUI/SearchBarNode.swift @@ -564,7 +564,7 @@ class SearchBarNode: ASDisplayNode, UITextFieldDelegate { } else if let cachedLayout = node.labelNode.cachedLayout { let labelNode = TextNode() labelNode.isOpaque = false - labelNode.isLayerBacked = true + labelNode.isUserInteractionEnabled = false let labelLayout = TextNode.asyncLayout(labelNode) let (labelLayoutResult, labelApply) = labelLayout(TextNodeLayoutArguments(attributedString: self.placeholderString, backgroundColor: .clear, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: cachedLayout.size, alignment: .natural, cutout: nil, insets: UIEdgeInsets())) let _ = labelApply() diff --git a/TelegramUI/SearchBarPlaceholderNode.swift b/TelegramUI/SearchBarPlaceholderNode.swift index 193dcde791..db0b9d8d51 100644 --- a/TelegramUI/SearchBarPlaceholderNode.swift +++ b/TelegramUI/SearchBarPlaceholderNode.swift @@ -57,7 +57,7 @@ class SearchBarPlaceholderNode: ASDisplayNode { self.labelNode = TextNode() self.labelNode.isOpaque = false - self.labelNode.isLayerBacked = true + self.labelNode.isUserInteractionEnabled = false super.init() diff --git a/TelegramUI/SecureIdAuthFormFieldNode.swift b/TelegramUI/SecureIdAuthFormFieldNode.swift index 4b30e1379c..ba32d21b38 100644 --- a/TelegramUI/SecureIdAuthFormFieldNode.swift +++ b/TelegramUI/SecureIdAuthFormFieldNode.swift @@ -784,12 +784,12 @@ final class SecureIdAuthFormFieldNode: ASDisplayNode { self.titleNode = ImmediateTextNode() self.titleNode.displaysAsynchronously = false - self.titleNode.isLayerBacked = true + self.titleNode.isUserInteractionEnabled = false self.titleNode.maximumNumberOfLines = 1 self.textNode = ImmediateTextNode() self.textNode.displaysAsynchronously = false - self.textNode.isLayerBacked = true + self.textNode.isUserInteractionEnabled = false self.textNode.maximumNumberOfLines = 4 self.disclosureNode = ASImageNode() diff --git a/TelegramUI/SettingsController.swift b/TelegramUI/SettingsController.swift index 1e9752a192..c6d6a0a7ee 100644 --- a/TelegramUI/SettingsController.swift +++ b/TelegramUI/SettingsController.swift @@ -1247,13 +1247,13 @@ public func settingsController(context: AccountContext, accountManager: AccountM var actions: [ContextMenuAction] = [] if let phone = user.phone, !phone.isEmpty { - actions.append(ContextMenuAction(content: .text(presentationData.strings.Settings_CopyPhoneNumber), action: { + actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Settings_CopyPhoneNumber, accessibilityLabel: presentationData.strings.Settings_CopyPhoneNumber), action: { UIPasteboard.general.string = formatPhoneNumber(phone) })) } if let username = user.username, !username.isEmpty { - actions.append(ContextMenuAction(content: .text(presentationData.strings.Settings_CopyUsername), action: { + actions.append(ContextMenuAction(content: .text(title: presentationData.strings.Settings_CopyUsername, accessibilityLabel: presentationData.strings.Settings_CopyUsername), action: { UIPasteboard.general.string = username })) } diff --git a/TelegramUI/StickerPackPreviewGridItem.swift b/TelegramUI/StickerPackPreviewGridItem.swift index 275ae6d4fc..4c06aa00dc 100644 --- a/TelegramUI/StickerPackPreviewGridItem.swift +++ b/TelegramUI/StickerPackPreviewGridItem.swift @@ -68,7 +68,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode { //self.imageNode.alphaTransitionOnFirstUpdate = true self.textNode = ASTextNode() - self.textNode.isLayerBacked = true + self.textNode.isUserInteractionEnabled = false self.textNode.displaysAsynchronously = true super.init() diff --git a/TelegramUI/ThemeAccentColorActionSheet.swift b/TelegramUI/ThemeAccentColorActionSheet.swift index 4635bbdf80..77c0f9022c 100644 --- a/TelegramUI/ThemeAccentColorActionSheet.swift +++ b/TelegramUI/ThemeAccentColorActionSheet.swift @@ -91,7 +91,7 @@ private final class ThemeAccentColorActionSheetItemNode: ActionSheetItemNode { self.titleNode = ImmediateTextNode() self.titleNode.displaysAsynchronously = false - self.titleNode.isLayerBacked = true + self.titleNode.isUserInteractionEnabled = false self.titleNode.attributedText = NSAttributedString(string: strings.Appearance_PickAccentColor, font: Font.medium(18.0), textColor: theme.primaryTextColor) self.items = [ diff --git a/TelegramUI/UserInfoController.swift b/TelegramUI/UserInfoController.swift index a618bd9fa9..8db753ab88 100644 --- a/TelegramUI/UserInfoController.swift +++ b/TelegramUI/UserInfoController.swift @@ -1328,7 +1328,7 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Us return false }) if let resultItemNode = resultItemNode { - let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(presentationData.strings.Conversation_ContextMenuCopy), action: { + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: presentationData.strings.Conversation_ContextMenuCopy), action: { UIPasteboard.general.string = text })]) strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in @@ -1359,7 +1359,7 @@ public func userInfoController(context: AccountContext, peerId: PeerId, mode: Us return false }) if let resultItemNode = resultItemNode { - let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(presentationData.strings.Conversation_ContextMenuCopy), action: { + let contextMenuController = ContextMenuController(actions: [ContextMenuAction(content: .text(title: presentationData.strings.Conversation_ContextMenuCopy, accessibilityLabel: presentationData.strings.Conversation_ContextMenuCopy), action: { UIPasteboard.general.string = value })]) strongController.present(contextMenuController, in: .window(.root), with: ContextMenuControllerPresentationArguments(sourceNodeAndRect: { [weak resultItemNode] in