diff --git a/CHANGES.rst b/CHANGES.rst index d86cb086f..cf35bf8b4 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,12 @@ +Changes in 0.9.5 (2019-09-20) +=============================================== + +Bug fix: + * VoiceOver: RoomVC: Fix some missing accessibility labels for buttons (#2722). + * VoiceOver: RoomVC: Make VoiceOver focus on the contextual menu when selecting an event (#2721). + * VoiceOver: RoomVC: Do not lose the focus on the timeline when paginating (with 3 fingers) (#2720). + * VoiceOver: RoomVC: No VoiceOver on media (#2726). + Changes in 0.9.4 (2019-09-13) =============================================== diff --git a/Riot/Assets/en.lproj/Vector.strings b/Riot/Assets/en.lproj/Vector.strings index fb1059c29..f26a6b6b6 100644 --- a/Riot/Assets/en.lproj/Vector.strings +++ b/Riot/Assets/en.lproj/Vector.strings @@ -250,6 +250,7 @@ // Chat "room_jump_to_first_unread" = "Jump to first unread message"; +"room_accessiblity_scroll_to_bottom" = "Scroll to bottom"; "room_new_message_notification" = "%d new message"; "room_new_messages_notification" = "%d new messages"; "room_one_user_is_typing" = "%@ is typing…"; @@ -317,6 +318,18 @@ "room_resource_usage_limit_reached_message_2" = "some users will not be able to log in."; "room_resource_usage_limit_reached_message_contact_3" = " to get this limit increased."; "room_message_edits_history_title" = "Message edits"; +"room_accessibility_search" = "Search"; +"room_accessibility_integrations" = "Integrations"; +"room_accessibility_upload" = "Upload"; +"room_accessibility_call" = "Call"; +"room_accessibility_hangup" = "Hang up"; + +"media_type_accessibility_image" = "Image"; +"media_type_accessibility_audio" = "Audio"; +"media_type_accessibility_video" = "Video"; +"media_type_accessibility_location" = "Location"; +"media_type_accessibility_file" = "File"; +"media_type_accessibility_sticker" = "Sticker"; // Unknown devices "unknown_devices_alert_title" = "Room contains unknown devices"; @@ -677,6 +690,9 @@ "widget_integration_missing_user_id" = "Missing user_id in request."; "widget_integration_room_not_visible" = "Room %@ is not visible."; +// Widget Picker +"widget_picker_title" = "Integrations"; + // Share extension "share_extension_auth_prompt" = "Login in the main app to share content"; "share_extension_failed_to_encrypt" = "Failed to send. Check in the main app the encryption settings for this room"; diff --git a/Riot/Generated/Strings.swift b/Riot/Generated/Strings.swift index caec20990..55da09c78 100644 --- a/Riot/Generated/Strings.swift +++ b/Riot/Generated/Strings.swift @@ -1374,6 +1374,30 @@ internal enum VectorL10n { internal static var mediaPickerTitle: String { return VectorL10n.tr("Vector", "media_picker_title") } + /// Audio + internal static var mediaTypeAccessibilityAudio: String { + return VectorL10n.tr("Vector", "media_type_accessibility_audio") + } + /// File + internal static var mediaTypeAccessibilityFile: String { + return VectorL10n.tr("Vector", "media_type_accessibility_file") + } + /// Image + internal static var mediaTypeAccessibilityImage: String { + return VectorL10n.tr("Vector", "media_type_accessibility_image") + } + /// Location + internal static var mediaTypeAccessibilityLocation: String { + return VectorL10n.tr("Vector", "media_type_accessibility_location") + } + /// Sticker + internal static var mediaTypeAccessibilitySticker: String { + return VectorL10n.tr("Vector", "media_type_accessibility_sticker") + } + /// Video + internal static var mediaTypeAccessibilityVideo: String { + return VectorL10n.tr("Vector", "media_type_accessibility_video") + } /// The Internet connection appears to be offline. internal static var networkOfflinePrompt: String { return VectorL10n.tr("Vector", "network_offline_prompt") @@ -1462,6 +1486,30 @@ internal enum VectorL10n { internal static var retry: String { return VectorL10n.tr("Vector", "retry") } + /// Call + internal static var roomAccessibilityCall: String { + return VectorL10n.tr("Vector", "room_accessibility_call") + } + /// Hang up + internal static var roomAccessibilityHangup: String { + return VectorL10n.tr("Vector", "room_accessibility_hangup") + } + /// Integrations + internal static var roomAccessibilityIntegrations: String { + return VectorL10n.tr("Vector", "room_accessibility_integrations") + } + /// Search + internal static var roomAccessibilitySearch: String { + return VectorL10n.tr("Vector", "room_accessibility_search") + } + /// Upload + internal static var roomAccessibilityUpload: String { + return VectorL10n.tr("Vector", "room_accessibility_upload") + } + /// Scroll to bottom + internal static var roomAccessiblityScrollToBottom: String { + return VectorL10n.tr("Vector", "room_accessiblity_scroll_to_bottom") + } /// Take photo or video internal static var roomActionCamera: String { return VectorL10n.tr("Vector", "room_action_camera") @@ -2974,6 +3022,10 @@ internal enum VectorL10n { internal static var widgetNoPowerToManage: String { return VectorL10n.tr("Vector", "widget_no_power_to_manage") } + /// Integrations + internal static var widgetPickerTitle: String { + return VectorL10n.tr("Vector", "widget_picker_title") + } /// You don't currently have any stickerpacks enabled. internal static var widgetStickerPickerNoStickerpacksAlert: String { return VectorL10n.tr("Vector", "widget_sticker_picker_no_stickerpacks_alert") diff --git a/Riot/Modules/Integrations/WidgetPicker/WidgetPickerViewController.m b/Riot/Modules/Integrations/WidgetPicker/WidgetPickerViewController.m index 4287370a3..d0077f87c 100644 --- a/Riot/Modules/Integrations/WidgetPicker/WidgetPickerViewController.m +++ b/Riot/Modules/Integrations/WidgetPicker/WidgetPickerViewController.m @@ -40,7 +40,7 @@ mxSession = theMXSession; roomId = theRoomId; - _alertController = [UIAlertController alertControllerWithTitle:@"Matrix Apps" + _alertController = [UIAlertController alertControllerWithTitle:NSLocalizedStringFromTable(@"widget_picker_title", @"Vector", nil) message:nil preferredStyle:UIAlertControllerStyleAlert]; } diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.h b/Riot/Modules/Room/CellData/RoomBubbleCellData.h index 777063a59..7c34278ff 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.h +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.h @@ -90,4 +90,9 @@ typedef NS_ENUM(NSInteger, RoomBubbleCellDataTag) - (BOOL)showAllReactionsForEvent:(NSString*)eventId; - (void)setShowAllReactions:(BOOL)showAllReactions forEvent:(NSString*)eventId; + +#pragma mark - Accessibility + +- (NSString*)accessibilityLabel; + @end diff --git a/Riot/Modules/Room/CellData/RoomBubbleCellData.m b/Riot/Modules/Room/CellData/RoomBubbleCellData.m index 5fda69f74..a89214ce4 100644 --- a/Riot/Modules/Room/CellData/RoomBubbleCellData.m +++ b/Riot/Modules/Room/CellData/RoomBubbleCellData.m @@ -698,4 +698,58 @@ static NSAttributedString *timestampVerticalWhitespace = nil; } } +- (NSString *)accessibilityLabel +{ + NSString *accessibilityLabel; + + // Only media require manual handling for accessibility + if (self.attachment) + { + NSString *mediaName = [self accessibilityLabelForAttachmentType:self.attachment.type]; + + MXJSONModelSetString(accessibilityLabel, self.events.firstObject.content[@"body"]); + if (accessibilityLabel) + { + accessibilityLabel = [NSString stringWithFormat:@"%@ %@", mediaName, accessibilityLabel]; + } + else + { + accessibilityLabel = mediaName; + } + } + + return accessibilityLabel; +} + +- (NSString*)accessibilityLabelForAttachmentType:(MXKAttachmentType)attachmentType +{ + NSString *accessibilityLabel; + switch (attachmentType) + { + case MXKAttachmentTypeImage: + accessibilityLabel = NSLocalizedStringFromTable(@"media_type_accessibility_image", @"Vector", nil); + break; + case MXKAttachmentTypeAudio: + accessibilityLabel = NSLocalizedStringFromTable(@"media_type_accessibility_audio", @"Vector", nil); + break; + case MXKAttachmentTypeVideo: + accessibilityLabel = NSLocalizedStringFromTable(@"media_type_accessibility_video", @"Vector", nil); + break; + case MXKAttachmentTypeLocation: + accessibilityLabel = NSLocalizedStringFromTable(@"media_type_accessibility_location", @"Vector", nil); + break; + case MXKAttachmentTypeFile: + accessibilityLabel = NSLocalizedStringFromTable(@"media_type_accessibility_file", @"Vector", nil); + break; + case MXKAttachmentTypeSticker: + accessibilityLabel = NSLocalizedStringFromTable(@"media_type_accessibility_sticker", @"Vector", nil); + break; + default: + accessibilityLabel = @""; + break; + } + + return accessibilityLabel; +} + @end diff --git a/Riot/Modules/Room/ContextualMenu/ContextualMenuItemView.swift b/Riot/Modules/Room/ContextualMenu/ContextualMenuItemView.swift index 8a0147e5f..2e95493d7 100644 --- a/Riot/Modules/Room/ContextualMenu/ContextualMenuItemView.swift +++ b/Riot/Modules/Room/ContextualMenu/ContextualMenuItemView.swift @@ -93,20 +93,30 @@ final class ContextualMenuItemView: UIView, NibOwnerLoadable { } // MARK: - Public - - func fill(title: String, image: UIImage?) { - self.originalImage = image?.withRenderingMode(.alwaysTemplate) - self.titleLabel.text = title - self.updateView() - } - + func fill(menuItem: RoomContextualMenuItem) { self.fill(title: menuItem.title, image: menuItem.image) + self.setupAccessibility(title: menuItem.title, isEnabled: menuItem.isEnabled) self.action = menuItem.action self.isEnabled = menuItem.isEnabled } // MARK: - Private + + private func fill(title: String, image: UIImage?) { + self.originalImage = image?.withRenderingMode(.alwaysTemplate) + self.titleLabel.text = title + self.updateView() + } + + private func setupAccessibility(title: String, isEnabled: Bool) { + self.isAccessibilityElement = true + self.accessibilityLabel = title + self.accessibilityTraits = .button + if !isEnabled { + self.accessibilityTraits.insert(.notEnabled) + } + } private func setupGestureRecognizer() { let gestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(buttonAction(_:))) diff --git a/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.swift b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.swift index 874ac0466..32052dc06 100644 --- a/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.swift +++ b/Riot/Modules/Room/ContextualMenu/RoomContextualMenuViewController.swift @@ -117,11 +117,15 @@ final class RoomContextualMenuViewController: UIViewController, Themable { func showMenuToolbar() { self.menuToolbarViewBottomConstraint.constant = 0 self.menuToolbarView.alpha = 1 + + // Force VoiceOver to focus on the menu bar actions + UIAccessibility.post(notification: .screenChanged, argument: self.menuToolbarView) } func hideMenuToolbar() { self.menuToolbarViewBottomConstraint.constant = self.hiddenToolbarViewBottomConstant self.menuToolbarView.alpha = 0 + UIAccessibility.post(notification: .screenChanged, argument: nil) } func prepareReactionsMenuAnimations() { diff --git a/Riot/Modules/Room/DataSources/RoomDataSource.m b/Riot/Modules/Room/DataSources/RoomDataSource.m index 3b05bc638..103fd0acc 100644 --- a/Riot/Modules/Room/DataSources/RoomDataSource.m +++ b/Riot/Modules/Room/DataSources/RoomDataSource.m @@ -196,6 +196,8 @@ if ([cell isKindOfClass:MXKRoomBubbleTableViewCell.class]) { MXKRoomBubbleTableViewCell *bubbleCell = (MXKRoomBubbleTableViewCell*)cell; + [self resetAccessibilityForCell:bubbleCell]; + RoomBubbleCellData *cellData = (RoomBubbleCellData*)bubbleCell.bubbleData; NSArray *bubbleComponents = cellData.bubbleComponents; @@ -507,6 +509,8 @@ // Auto animate the sticker in case of animated gif bubbleCell.isAutoAnimatedGif = (cellData.attachment && cellData.attachment.type == MXKAttachmentTypeSticker); + + [self setupAccessibilityForCell:bubbleCell withCellData:cellData]; } return cell; @@ -563,6 +567,35 @@ [self sendVideo:videoLocalURL withThumbnail:videoThumbnail success:success failure:failure]; } + +#pragma - Accessibility + +- (void)setupAccessibilityForCell:(MXKRoomBubbleTableViewCell *)cell withCellData:(RoomBubbleCellData*)cellData +{ + // Set accessibility only on media. Let VoiceOver automatically manages text messages + if (cellData.attachment) + { + NSString *accessibilityLabel = [cellData accessibilityLabel]; + if (cell.messageTextView.text.length) + { + // Files are presented as text with link + cell.messageTextView.accessibilityLabel = accessibilityLabel; + cell.messageTextView.isAccessibilityElement = YES; + } + else + { + cell.attachmentView.accessibilityLabel = accessibilityLabel; + cell.attachmentView.isAccessibilityElement = YES; + } + } +} + +- (void)resetAccessibilityForCell:(MXKRoomBubbleTableViewCell *)cell +{ + cell.messageTextView.accessibilityLabel = nil; + cell.attachmentView.accessibilityLabel = nil; +} + #pragma mark - BubbleReactionsViewModelDelegate - (void)bubbleReactionsViewModel:(BubbleReactionsViewModel *)viewModel didAddReaction:(MXReactionCount *)reactionCount forEventId:(NSString *)eventId diff --git a/Riot/Modules/Room/RoomViewController.m b/Riot/Modules/Room/RoomViewController.m index 46327a0e8..974fe125e 100644 --- a/Riot/Modules/Room/RoomViewController.m +++ b/Riot/Modules/Room/RoomViewController.m @@ -123,7 +123,7 @@ #import "Riot-Swift.h" -@interface RoomViewController () { @@ -813,6 +813,92 @@ [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; } +#pragma mark - Accessibility + +// Handle scrolling when VoiceOver is on because it does not work well if we let the system do: +// VoiceOver loses the focus on the tableview +- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction +{ + BOOL canScroll = YES; + + // Scroll by one page + CGFloat tableViewHeight = self.bubblesTableView.frame.size.height; + + CGPoint offset = self.bubblesTableView.contentOffset; + switch (direction) + { + case UIAccessibilityScrollDirectionUp: + offset.y -= tableViewHeight; + break; + + case UIAccessibilityScrollDirectionDown: + offset.y += tableViewHeight; + break; + + default: + break; + } + + if (offset.y < 0 && ![self.roomDataSource.timeline canPaginate:MXTimelineDirectionBackwards]) + { + // Can't paginate more. Let's stick on the first item + UIView *focusedView = [self firstCellWithAccessibilityDataInCells:self.bubblesTableView.visibleCells.objectEnumerator]; + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, focusedView); + canScroll = NO; + } + else if (offset.y > self.bubblesTableView.contentSize.height - tableViewHeight + && ![self.roomDataSource.timeline canPaginate:MXTimelineDirectionForwards]) + { + // Can't paginate more. Let's stick on the last item with accessibility + UIView *focusedView = [self firstCellWithAccessibilityDataInCells:self.bubblesTableView.visibleCells.reverseObjectEnumerator]; + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, focusedView); + canScroll = NO; + } + else + { + // Disable VoiceOver while scrolling + self.bubblesTableView.accessibilityElementsHidden = YES; + + [self.bubblesTableView setContentOffset:offset animated:NO]; + + NSEnumerator *cells; + if (direction == UIAccessibilityScrollDirectionUp) + { + cells = self.bubblesTableView.visibleCells.objectEnumerator; + } + else + { + cells = self.bubblesTableView.visibleCells.reverseObjectEnumerator; + } + UIView *cell = [self firstCellWithAccessibilityDataInCells:cells]; + + self.bubblesTableView.accessibilityElementsHidden = NO; + + // Force VoiceOver to focus on a visible item + UIAccessibilityPostNotification(UIAccessibilityLayoutChangedNotification, cell); + } + + // If we cannot scroll, let VoiceOver indicates the border + return canScroll; +} + +- (UIView*)firstCellWithAccessibilityDataInCells:(NSEnumerator*)cells +{ + UIView *view; + + for (UITableViewCell *cell in cells) + { + if (![cell isKindOfClass:[RoomEmptyBubbleCell class]]) + { + view = cell; + break; + } + } + + return view; +} + + #pragma mark - Override MXKRoomViewController - (void)onMatrixSessionChange @@ -1372,12 +1458,16 @@ icon = [icon imageWithRenderingMode:UIImageRenderingModeAlwaysOriginal]; self.navigationItem.rightBarButtonItems[1].image = icon; + self.navigationItem.rightBarButtonItems[1].accessibilityLabel = NSLocalizedStringFromTable(@"room_accessibility_integrations", @"Vector", nil); } else { // Reset original icon self.navigationItem.rightBarButtonItems[1].image = [UIImage imageNamed:@"apps-icon"]; + self.navigationItem.rightBarButtonItems[1].accessibilityLabel = NSLocalizedStringFromTable(@"room_accessibility_integrations", @"Vector", nil); } + + self.navigationItem.rightBarButtonItems.firstObject.accessibilityLabel = NSLocalizedStringFromTable(@"room_accessibility_search", @"Vector", nil); } // Do not change title view class here if the expanded header is visible. diff --git a/Riot/Modules/Room/Views/Activities/RoomActivitiesView.m b/Riot/Modules/Room/Views/Activities/RoomActivitiesView.m index 6648db9b1..1baaf2079 100644 --- a/Riot/Modules/Room/Views/Activities/RoomActivitiesView.m +++ b/Riot/Modules/Room/Views/Activities/RoomActivitiesView.m @@ -307,6 +307,11 @@ self.iconImageView.image = [UIImage imageNamed:@"scrolldown"]; } self.iconImageView.hidden = NO; + + // Make VoiceOver consider it as a button + self.iconImageView.accessibilityLabel = NSLocalizedStringFromTable(@"room_accessiblity_scroll_to_bottom", @"Vector", nil); + self.iconImageView.isAccessibilityElement = YES; + self.iconImageView.accessibilityTraits = UIAccessibilityTraitButton; if (onIconTapGesture) { diff --git a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m index 577e89544..855c21454 100644 --- a/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m +++ b/Riot/Modules/Room/Views/InputToolbar/RoomInputToolbarView.m @@ -92,6 +92,10 @@ growingTextView.tintColor = ThemeService.shared.theme.tintColor; growingTextView.internalTextView.keyboardAppearance = ThemeService.shared.theme.keyboardAppearance; + + self.attachMediaButton.accessibilityLabel = NSLocalizedStringFromTable(@"room_accessibility_upload", @"Vector", nil); + self.voiceCallButton.accessibilityLabel = NSLocalizedStringFromTable(@"room_accessibility_call", @"Vector", nil); + self.hangupCallButton.accessibilityLabel = NSLocalizedStringFromTable(@"room_accessibility_hangup", @"Vector", nil); } #pragma mark - diff --git a/Riot/SupportingFiles/Info.plist b/Riot/SupportingFiles/Info.plist index 8f43f1a7c..5a0c589e5 100644 --- a/Riot/SupportingFiles/Info.plist +++ b/Riot/SupportingFiles/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.9.4 + 0.9.5 CFBundleSignature ???? CFBundleVersion - 0.9.4 + 0.9.5 ITSAppUsesNonExemptEncryption ITSEncryptionExportComplianceCode diff --git a/RiotShareExtension/SupportingFiles/Info.plist b/RiotShareExtension/SupportingFiles/Info.plist index 2554ed278..ba2f5a2e1 100644 --- a/RiotShareExtension/SupportingFiles/Info.plist +++ b/RiotShareExtension/SupportingFiles/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 0.9.4 + 0.9.5 CFBundleVersion - 0.9.4 + 0.9.5 NSExtension NSExtensionAttributes diff --git a/SiriIntents/Info.plist b/SiriIntents/Info.plist index af2bd2424..b39ef1a68 100644 --- a/SiriIntents/Info.plist +++ b/SiriIntents/Info.plist @@ -17,9 +17,9 @@ CFBundlePackageType XPC! CFBundleShortVersionString - 0.9.4 + 0.9.5 CFBundleVersion - 0.9.4 + 0.9.5 NSExtension NSExtensionAttributes