Skip to content

Commit dd0b6fa

Browse files
OtavioStasiakcoderabbitai[bot]diegolmello
authored
feat(a11y): in-app notifications announcement and incoming call (#6523)
Co-authored-by: OtavioStasiak <[email protected]> Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> Co-authored-by: Diego Mello <[email protected]>
1 parent 55e7db3 commit dd0b6fa

File tree

32 files changed

+448
-142
lines changed

32 files changed

+448
-142
lines changed

app/containers/CallHeader.tsx

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22
import { StyleSheet, Text, View } from 'react-native';
33
import Touchable from 'react-native-platform-touchable';
4+
import { A11y } from 'react-native-a11y-order';
45

56
import { useAppSelector } from '../lib/hooks/useAppSelector';
67
import { useTheme } from '../theme';
@@ -10,6 +11,7 @@ import { BUTTON_HIT_SLOP } from './message/utils';
1011
import AvatarContainer from './Avatar';
1112
import StatusContainer from './Status';
1213
import DotsLoader from './DotsLoader';
14+
import I18n from '../i18n';
1315

1416
type TCallHeader = {
1517
mic: boolean;
@@ -38,37 +40,45 @@ export const CallHeader = ({ mic, cam, setCam, setMic, title, avatar, uid, name,
3840
};
3941

4042
return (
41-
<View>
42-
<View style={style.actionSheetHeader}>
43-
<View style={style.rowContainer}>
44-
<Text style={style.actionSheetHeaderTitle}>{title}</Text>
45-
{calling && direct ? <DotsLoader /> : null}
43+
<A11y.Order>
44+
<View>
45+
<View style={style.actionSheetHeader}>
46+
<View style={style.rowContainer}>
47+
<Text style={style.actionSheetHeaderTitle}>{title}</Text>
48+
{calling && direct ? <DotsLoader /> : null}
49+
</View>
50+
<View style={style.actionSheetHeaderButtons}>
51+
<A11y.Index index={1}>
52+
<Touchable
53+
accessibilityLabel={cam ? I18n.t('Turn_camera_off') : I18n.t('Turn_camera_on')}
54+
onPress={() => setCam(!cam)}
55+
style={[style.iconCallContainerRight, { backgroundColor: handleColors(cam).button }]}
56+
hitSlop={BUTTON_HIT_SLOP}
57+
disabled={calling}>
58+
<CustomIcon name={cam ? 'camera' : 'camera-disabled'} size={24} color={handleColors(cam).icon} />
59+
</Touchable>
60+
</A11y.Index>
61+
<A11y.Index index={2}>
62+
<Touchable
63+
accessibilityLabel={mic ? I18n.t('Turn_mic_off') : I18n.t('Turn_mic_on')}
64+
onPress={() => setMic(!mic)}
65+
style={[style.iconCallContainer, { backgroundColor: handleColors(mic).button }]}
66+
hitSlop={BUTTON_HIT_SLOP}
67+
disabled={calling}>
68+
<CustomIcon name={mic ? 'microphone' : 'microphone-disabled'} size={24} color={handleColors(mic).icon} />
69+
</Touchable>
70+
</A11y.Index>
71+
</View>
4672
</View>
47-
<View style={style.actionSheetHeaderButtons}>
48-
<Touchable
49-
onPress={() => setCam(!cam)}
50-
style={[style.iconCallContainerRight, { backgroundColor: handleColors(cam).button }]}
51-
hitSlop={BUTTON_HIT_SLOP}
52-
disabled={calling}>
53-
<CustomIcon name={cam ? 'camera' : 'camera-disabled'} size={24} color={handleColors(cam).icon} />
54-
</Touchable>
55-
<Touchable
56-
onPress={() => setMic(!mic)}
57-
style={[style.iconCallContainer, { backgroundColor: handleColors(mic).button }]}
58-
hitSlop={BUTTON_HIT_SLOP}
59-
disabled={calling}>
60-
<CustomIcon name={mic ? 'microphone' : 'microphone-disabled'} size={24} color={handleColors(mic).icon} />
61-
</Touchable>
73+
<View style={style.actionSheetUsernameContainer}>
74+
<AvatarContainer text={avatar} size={36} />
75+
{direct ? <StatusContainer size={16} id={uid} style={style.statusContainerMargin} /> : null}
76+
<Text style={{ ...style.actionSheetUsername, marginLeft: !direct ? 8 : 0 }} numberOfLines={1}>
77+
{name}
78+
</Text>
6279
</View>
6380
</View>
64-
<View style={style.actionSheetUsernameContainer}>
65-
<AvatarContainer text={avatar} size={36} />
66-
{direct ? <StatusContainer size={16} id={uid} style={style.statusContainerMargin} /> : null}
67-
<Text style={{ ...style.actionSheetUsername, marginLeft: !direct ? 8 : 0 }} numberOfLines={1}>
68-
{name}
69-
</Text>
70-
</View>
71-
</View>
81+
</A11y.Order>
7282
);
7383
};
7484

app/containers/InAppNotification/IncomingCallNotification/index.tsx

Lines changed: 84 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import React, { useState } from 'react';
2-
import { Text, View } from 'react-native';
1+
import React, { useEffect, useRef, useState } from 'react';
2+
import { AccessibilityInfo, findNodeHandle, Text, View } from 'react-native';
33
import Touchable from 'react-native-platform-touchable';
44
import { useSafeAreaInsets } from 'react-native-safe-area-context';
55
import { useDispatch } from 'react-redux';
6+
import { A11y } from 'react-native-a11y-order';
67

78
import { acceptCall, cancelCall } from '../../../actions/videoConf';
89
import { type ISubscription, type SubscriptionType } from '../../../definitions';
@@ -33,6 +34,7 @@ const BUTTON_HIT_SLOP = { top: 12, right: 12, bottom: 12, left: 12 };
3334

3435
const IncomingCallHeader = React.memo(
3536
({ uid, callId, avatar, roomName }: { callId: string; avatar: string; uid: string; roomName: string }) => {
37+
const componentRef = useRef<View>(null);
3638
const [mic, setMic] = useState(true);
3739
const [cam, setCam] = useState(false);
3840
const [audio, setAudio] = useState(true);
@@ -41,59 +43,87 @@ const IncomingCallHeader = React.memo(
4143
const styles = useStyle();
4244
const insets = useSafeAreaInsets();
4345

46+
useEffect(() => {
47+
const focusOnIncomingCall = setTimeout(() => {
48+
const node = findNodeHandle(componentRef.current);
49+
if (node) {
50+
AccessibilityInfo.setAccessibilityFocus(node);
51+
}
52+
}, 300);
53+
54+
return () => clearTimeout(focusOnIncomingCall);
55+
}, [uid, callId, avatar, roomName]);
56+
4457
return (
45-
<View
46-
style={[
47-
styles.container,
48-
isMasterDetail && styles.small,
49-
{
50-
marginTop: insets.top
51-
}
52-
]}>
53-
<CallHeader
54-
title={i18n.t('Incoming_call_from')}
55-
cam={cam}
56-
setCam={setCam}
57-
mic={mic}
58-
setMic={setMic}
59-
avatar={avatar}
60-
name={roomName}
61-
uid={uid}
62-
direct={true}
63-
/>
64-
<View style={styles.row}>
65-
<Touchable
66-
hitSlop={BUTTON_HIT_SLOP}
67-
onPress={() => {
68-
setAudio(!audio);
69-
hideNotification();
70-
}}
71-
style={styles.closeButton}>
72-
<CustomIcon name='close' size={20} />
73-
</Touchable>
74-
<Touchable
75-
hitSlop={BUTTON_HIT_SLOP}
76-
onPress={() => {
77-
setAudio(!audio);
78-
hideNotification();
79-
dispatch(cancelCall({ callId }));
80-
}}
81-
style={styles.cancelButton}>
82-
<Text style={styles.buttonText}>{i18n.t('decline')}</Text>
83-
</Touchable>
84-
<Touchable
85-
hitSlop={BUTTON_HIT_SLOP}
86-
onPress={() => {
87-
setAudio(!audio);
88-
hideNotification();
89-
dispatch(acceptCall({ callId }));
90-
}}
91-
style={styles.acceptButton}>
92-
<Text style={styles.buttonText}>{i18n.t('accept')}</Text>
93-
</Touchable>
94-
</View>
95-
{audio ? <Ringer ringer={ERingerSounds.RINGTONE} /> : null}
96-
</View>
58+
<A11y.Order>
59+
<A11y.Index index={1}>
60+
<View
61+
ref={componentRef}
62+
accessible={true}
63+
accessibilityRole='button'
64+
accessibilityLabel={`${i18n.t('Incoming_call_from')} ${roomName}`}
65+
style={[
66+
styles.container,
67+
isMasterDetail && styles.small,
68+
{
69+
marginTop: insets.top
70+
}
71+
]}>
72+
<A11y.Index index={2} style={{ flex: 1 }}>
73+
<CallHeader
74+
title={i18n.t('Incoming_call_from')}
75+
cam={cam}
76+
setCam={setCam}
77+
mic={mic}
78+
setMic={setMic}
79+
avatar={avatar}
80+
name={roomName}
81+
uid={uid}
82+
direct={true}
83+
/>
84+
</A11y.Index>
85+
<View style={styles.row}>
86+
<A11y.Index index={3} style={{ flex: 1 }}>
87+
<Touchable
88+
hitSlop={BUTTON_HIT_SLOP}
89+
onPress={() => {
90+
setAudio(!audio);
91+
hideNotification();
92+
}}
93+
accessibilityLabel={i18n.t('A11y_incoming_call_dismiss')}
94+
style={styles.closeButton}>
95+
<CustomIcon name='close' size={20} />
96+
</Touchable>
97+
</A11y.Index>
98+
<A11y.Index index={4} style={{ flex: 1 }}>
99+
<Touchable
100+
hitSlop={BUTTON_HIT_SLOP}
101+
onPress={() => {
102+
setAudio(!audio);
103+
hideNotification();
104+
dispatch(cancelCall({ callId }));
105+
}}
106+
style={styles.cancelButton}>
107+
<Text style={styles.buttonText}>{i18n.t('decline')}</Text>
108+
</Touchable>
109+
</A11y.Index>
110+
<A11y.Index index={5} style={{ flex: 1 }}>
111+
<Touchable
112+
hitSlop={BUTTON_HIT_SLOP}
113+
onPress={() => {
114+
setAudio(!audio);
115+
hideNotification();
116+
dispatch(acceptCall({ callId }));
117+
}}
118+
style={styles.acceptButton}>
119+
<Text style={styles.buttonText}>{i18n.t('accept')}</Text>
120+
</Touchable>
121+
</A11y.Index>
122+
</View>
123+
{audio ? <Ringer ringer={ERingerSounds.RINGTONE} /> : null}
124+
</View>
125+
</A11y.Index>
126+
</A11y.Order>
97127
);
98128
}
99129
);

app/containers/InAppNotification/NotifierComponent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export interface INotifierComponent {
2020
payload: {
2121
sender: { username: string };
2222
type: SubscriptionType;
23-
message?: { message: string; t?: string };
23+
message?: { message?: string; msg?: string; t?: string };
2424
} & Pick<ISubscription, '_id' | 'name' | 'rid' | 'prid'>;
2525
title: string;
2626
avatar: string;

app/containers/InAppNotification/index.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import React, { type ElementType, memo, useEffect } from 'react';
22
import { Easing, Notifier, NotifierRoot } from 'react-native-notifier';
33
import { useDispatch } from 'react-redux';
4+
import { AccessibilityInfo } from 'react-native';
45

56
import NotifierComponent, { type INotifierComponent } from './NotifierComponent';
67
import EventEmitter from '../../lib/methods/helpers/events';
78
import Navigation from '../../lib/navigation/appNavigation';
89
import { getActiveRoute } from '../../lib/methods/helpers/navigation';
910
import { useAppSelector } from '../../lib/hooks/useAppSelector';
1011
import { setInAppFeedback } from '../../actions/inAppFeedback';
12+
import I18n from '../../i18n';
1113

1214
export const INAPP_NOTIFICATION_EMITTER = 'NotificationInApp';
1315

@@ -42,6 +44,15 @@ const InAppNotification = memo(() => {
4244
return;
4345
}
4446

47+
if (payload?.name && payload?.message) {
48+
AccessibilityInfo.announceForAccessibility(
49+
I18n.t('A11y_in_app_notification', {
50+
name: payload?.name || payload?.sender?.username || '',
51+
message: payload?.message?.message || payload?.message?.msg || ''
52+
})
53+
);
54+
}
55+
4556
Notifier.showNotification({
4657
showEasing: Easing.inOut(Easing.quad),
4758
Component: notification.customComponent || NotifierComponent,

app/i18n/locales/ar.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
"A11y_appearance_show_alerts_as": "عرض التنبيهات كـ",
66
"A11y_appearance_toast_dismissed_automatically": "تم رفضه تلقائيًا",
77
"A11y_appearance_toasts": "إشعارات منبثقة (Toast)",
8+
"A11y_in_app_notification": "رسالة جديدة من {{name}}: {{message}}",
9+
"A11y_incoming_call_dismiss": "تجاهل",
10+
"A11y_incoming_call_swipe_down_to_view_options": "اسحب لأسفل لعرض الخيارات",
811
"Accessibility": "إمكانية الوصول",
912
"Accessibility_and_Appearance": "إمكانية الوصول والمظهر",
1013
"Accessibility_statement": "بيان الوصول",
@@ -64,6 +67,7 @@
6467
"Busy": "مشغول",
6568
"Call_already_ended": "تم انهاء المكالمة بالفعل !",
6669
"Call_room_name": "اتصل بـ {{roomName}}",
70+
"Call_was_canceled_before_being_answered": "تم إلغاء المكالمة قبل الرد عليها",
6771
"Cancel": "إلغاء",
6872
"Cancel_and_delete_recording": "إلغاء وحذف التسجيل",
6973
"Cancel_editing": "إلغاء التعديل",
@@ -135,8 +139,6 @@
135139
"Department": "القسم",
136140
"Description": "وصف",
137141
"description": "وصف",
138-
"Desktop_Alert_info": "هذه الإشعارات ترسل لسطح المكتب",
139-
"Desktop_Notifications": "إخطارات سطح المكتب",
140142
"Direct_message_someone": "رسالة مباشرة لشخص ما",
141143
"Direct_Messages": "رسالة مباشرة",
142144
"Directory": "مجلد",
@@ -223,15 +225,18 @@
223225
"Glossary_of_simplified_terms": "مسرد المصطلحات المبسطة",
224226
"Help": "مساعدة",
225227
"Hide": "إخفاء",
228+
"Hide_notification": "إخفاء الإشعار",
226229
"Hide_Password": "إخفاء كلمة المرور",
227230
"Hide_room": "إخفاء",
228231
"Hide_System_Messages": "إخفاء رسائل النظام",
229232
"Hide_type_messages": "إخفاء رسائل \"{{type}}\"",
230233
"How_It_Works": "طريقة العمل",
231234
"I_Saved_My_E2E_Password": "قمت بحفظ كلمة المرور الطرفية",
232235
"In_app": "في التطبيق",
236+
"In_App_Alert_info": "يتم تسليم هذه الإشعارات لك عندما يكون التطبيق مفتوحًا.",
233237
"In_App_And_Desktop": "تطبيق داخلي وسطح المكتب",
234238
"In_App_and_Desktop_Alert_info": "يعرض شعاراً أعلى الشاشة عندما يكون التطبيق مفتوحًا، ويعرض إشعاراً على سطح المكتب",
239+
"In_App_Notification": "إشعار داخل التطبيق",
235240
"Insert_Join_Code": "ضع رمز الانضمام",
236241
"Invalid_code": "رمز غير صالح",
237242
"Invalid_or_expired_invite_token": "رمز الدعوة غير صالح أو منتهي الصلاحية",
@@ -357,6 +362,7 @@
357362
"Nothing_to_save": "لا شيء للحفظ!",
358363
"Notification_Preferences": "تفضيلات الإشعار",
359364
"Notifications": "الإشعارات",
365+
"Notifications_vibrate_from_new_messages": "اهتزاز للرسائل الجديدة",
360366
"Notify_active_in_this_room": "أبلغ المستخدمين النشطين في هذه الغرفة",
361367
"Notify_all_in_this_room": "أبلغ الجميع في الغرفة",
362368
"Objects": "أشياء",
@@ -550,6 +556,10 @@
550556
"Translate": "ترجمة",
551557
"Travel_and_places": "السفر والأماكن",
552558
"Try_again": "حاول مجدداً",
559+
"Turn_camera_off": "إيقاف تشغيل الكاميرا",
560+
"Turn_camera_on": "تشغيل الكاميرا",
561+
"Turn_mic_off": "أوقف تشغيل الميكروفون",
562+
"Turn_mic_on": "تشغيل الميكروفون",
553563
"Two_Factor_Authentication": "المصادقة الثنائية",
554564
"Two_Factor_Success_message": "تم إرسال رمز التحقق الثنائي العوامل! يرجى التحقق من بريدك الإلكتروني.",
555565
"Type_message": "اكتب رسالة",

0 commit comments

Comments
 (0)