@eohjsc/react-native-smart-city 0.2.89 → 0.2.92

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/package.json +1 -1
  2. package/src/commons/ActionGroup/ColorPickerTemplate.js +30 -24
  3. package/src/commons/ActionGroup/OnOffSmartLock/OnOffSmartLock.js +60 -12
  4. package/src/commons/ActionGroup/OnOffSmartLock/PasscodeList/__test__/index.test.js +47 -0
  5. package/src/commons/ActionGroup/OnOffSmartLock/PasscodeList/index.js +2 -0
  6. package/src/commons/ActionGroup/OnOffTemplate/index.js +48 -28
  7. package/src/commons/ActionGroup/SliderRangeTemplate.js +19 -5
  8. package/src/commons/ActionGroup/__test__/ColorPickerTemplate.test.js +74 -0
  9. package/src/commons/ActionGroup/__test__/OnOffSmartLock.test.js +107 -0
  10. package/src/commons/ActionGroup/__test__/SliderRangeTemplate.test.js +71 -0
  11. package/src/commons/ActionGroup/index.js +1 -1
  12. package/src/commons/Calendar/index.js +5 -1
  13. package/src/commons/ConnectingProcess/__test__/Connecting.test.js +4 -1
  14. package/src/commons/Device/HorizontalBarChart.js +6 -2
  15. package/src/commons/SubUnit/Favorites/index.js +4 -35
  16. package/src/commons/SubUnit/ShortDetail.js +7 -41
  17. package/src/configs/Constants.js +5 -0
  18. package/src/hooks/Common/index.js +2 -0
  19. package/src/hooks/Common/useSensorsStatus.js +52 -0
  20. package/src/screens/AddNewGateway/PlugAndPlay/__test__/ConnectWifiWarning.test.js +1 -0
  21. package/src/screens/AddNewGateway/__test__/SelectGateway.test.js +61 -0
  22. package/src/screens/Device/__test__/DetailHistoryChart.test.js +40 -0
  23. package/src/screens/Device/detail.js +46 -29
  24. package/src/screens/HanetCamera/Detail.js +20 -13
  25. package/src/screens/HanetCamera/ManageAccess.js +10 -52
  26. package/src/screens/HanetCamera/MemberInfo.js +59 -13
  27. package/src/screens/HanetCamera/__test__/ManageAccess.test.js +19 -0
  28. package/src/screens/HanetCamera/__test__/MemberInfo.test.js +57 -10
  29. package/src/screens/HanetCamera/components/RequestFaceIDPopup.js +90 -0
  30. package/src/screens/HanetCamera/hooks/__test__/useHanetCheckinData.test.js +9 -12
  31. package/src/screens/HanetCamera/hooks/useHanetCheckinData.js +10 -7
  32. package/src/screens/HanetCamera/styles/manageAccessStyles.js +0 -14
  33. package/src/screens/Unit/Detail.js +1 -7
  34. package/src/screens/Unit/components/MyUnitDevice/index.js +13 -10
  35. package/src/screens/Unit/components/__test__/MyUnitDevice.test.js +2 -2
  36. package/src/utils/I18n/translations/en.json +1 -0
  37. package/src/utils/I18n/translations/vi.json +1 -0
@@ -7,7 +7,9 @@ import { AlertAction, FullLoading } from '../../commons';
7
7
  import BottomButtonView from '../../commons/BottomButtonView';
8
8
  import TextInput from '../../commons/Form/TextInput';
9
9
  import Text from '../../commons/Text';
10
+ import RequestFaceIDPopup from './components/RequestFaceIDPopup';
10
11
  import { useStateAlertAction } from './hooks';
12
+ import { useBoolean } from '../../hooks/Common';
11
13
  import { useTranslations } from '../../hooks/Common/useTranslations';
12
14
  import useKeyboardAnimated from '../../hooks/Explore/useKeyboardAnimated';
13
15
  import styles from './styles/memberInfoStyles';
@@ -30,19 +32,10 @@ const HanetMemberInfo = ({ route }) => {
30
32
  setMemberName(hanetMember.name);
31
33
  }, [hanetMember]);
32
34
 
33
- const { stateAlertAction, hideAlertAction, showRename, showDelete } =
34
- useStateAlertAction();
35
-
36
- const onShowRename = () => {
37
- setInputName(memberName);
38
- showRename(isAddNewMember);
39
- };
40
-
41
- const onShowDelete = () => {
42
- showDelete();
43
- };
35
+ const [showSetFaceIDModal, setShowSetFaceIDModal, setHideSetFaceIDModal] =
36
+ useBoolean();
44
37
 
45
- const setFaceID = () => {
38
+ const onCaptureFaceID = () => {
46
39
  navigate(Routes.HanetCaptureFaceID, {
47
40
  title: t('set_photo_id'),
48
41
  hanetPlace,
@@ -57,6 +50,52 @@ const HanetMemberInfo = ({ route }) => {
57
50
  });
58
51
  };
59
52
 
53
+ const onChooseFile = async (result) => {
54
+ const imageUri = result.path;
55
+ if (isAddNewMember) {
56
+ setMemberAvatar(imageUri);
57
+ return;
58
+ }
59
+
60
+ const formData = new FormData();
61
+ const name = imageUri.split('/').pop();
62
+ const ext = imageUri.split('.').pop();
63
+ formData.append('face_id', {
64
+ name: name,
65
+ type: `image/${ext}`,
66
+ uri: imageUri,
67
+ });
68
+ setLoading(true);
69
+ const { success, data } = await axiosPatch(
70
+ API.CAMERA.HANET.UPDATE_FACE_ID(
71
+ hanetPlace.place_id,
72
+ hanetMember.alias_id
73
+ ),
74
+ formData,
75
+ {
76
+ headers: { 'Content-Type': 'multipart/form-data' },
77
+ }
78
+ );
79
+ if (success) {
80
+ setMemberAvatar(data.avatar_uri);
81
+ } else {
82
+ setError(data.detail);
83
+ }
84
+ setLoading(false);
85
+ };
86
+
87
+ const { stateAlertAction, hideAlertAction, showRename, showDelete } =
88
+ useStateAlertAction();
89
+
90
+ const onShowRename = () => {
91
+ setInputName(memberName);
92
+ showRename(isAddNewMember);
93
+ };
94
+
95
+ const onShowDelete = () => {
96
+ showDelete();
97
+ };
98
+
60
99
  const finishRegister = async () => {
61
100
  setLoading(true);
62
101
  const formData = new FormData();
@@ -135,7 +174,7 @@ const HanetMemberInfo = ({ route }) => {
135
174
  <Image source={{ uri: memberAvatar }} style={styles.avatar} />
136
175
  </View>
137
176
  <TouchableOpacity
138
- onPress={setFaceID}
177
+ onPress={setShowSetFaceIDModal}
139
178
  style={[styles.row, !!error && { borderBottomColor: Colors.Red6 }]}
140
179
  >
141
180
  <IconFill name="camera" size={27} color={Colors.Primary} />
@@ -201,6 +240,13 @@ const HanetMemberInfo = ({ route }) => {
201
240
  />
202
241
  )}
203
242
  </AlertAction>
243
+ <RequestFaceIDPopup
244
+ title={t('set_photo_id')}
245
+ isVisible={showSetFaceIDModal}
246
+ setHide={setHideSetFaceIDModal}
247
+ onCaptureFaceID={onCaptureFaceID}
248
+ onChooseFile={onChooseFile}
249
+ />
204
250
  </View>
205
251
  );
206
252
  };
@@ -8,6 +8,7 @@ import HanetManageAccess from '../ManageAccess';
8
8
  import BottomSheet from '../../../commons/BottomSheet';
9
9
  import { TESTID } from '../../../configs/Constants';
10
10
  import Routes from '../../../utils/Route';
11
+ import ImagePicker from 'react-native-image-crop-picker';
11
12
 
12
13
  const wrapComponent = (route) => (
13
14
  <SCProvider initState={mockSCStore({})}>
@@ -149,4 +150,22 @@ describe('Test HanetManageAccess', () => {
149
150
  isAddNewMember: true,
150
151
  });
151
152
  });
153
+
154
+ test('Test add new member choose file error', async () => {
155
+ await act(async () => {
156
+ tree = await create(wrapComponent(route));
157
+ });
158
+ const instance = tree.root;
159
+
160
+ const bottomSheet = instance.findByType(BottomSheet);
161
+
162
+ ImagePicker.openPicker.mockImplementationOnce(async () => {
163
+ throw new Error();
164
+ });
165
+
166
+ await chooseAddMemberOption(instance, 1);
167
+
168
+ expect(bottomSheet.props.isVisible).toBe(false);
169
+ expect(mockedNavigate).not.toBeCalled();
170
+ });
152
171
  });
@@ -8,6 +8,8 @@ import HanetMemberInfo from '../MemberInfo';
8
8
  import { AlertAction } from '../../../commons';
9
9
  import TextInput from '../../../commons/Form/TextInput';
10
10
  import BottomButtonView from '../../../commons/BottomButtonView';
11
+ import BottomSheet from '../../../commons/BottomSheet';
12
+ import { TESTID } from '../../../configs/Constants';
11
13
 
12
14
  const wrapComponent = (route) => (
13
15
  <SCProvider initState={mockSCStore({})}>
@@ -68,7 +70,7 @@ describe('Test HanetMemberInfo', () => {
68
70
  });
69
71
  const instance = tree.root;
70
72
  const touches = instance.findAllByType(TouchableOpacity);
71
- expect(touches).toHaveLength(5);
73
+ expect(touches).toHaveLength(7);
72
74
  const alertAction = instance.findByType(AlertAction);
73
75
 
74
76
  // open alert action
@@ -95,7 +97,7 @@ describe('Test HanetMemberInfo', () => {
95
97
  });
96
98
  const instance = tree.root;
97
99
  const touches = instance.findAllByType(TouchableOpacity);
98
- expect(touches).toHaveLength(5);
100
+ expect(touches).toHaveLength(7);
99
101
  const alertAction = instance.findByType(AlertAction);
100
102
 
101
103
  // open alert action
@@ -115,41 +117,86 @@ describe('Test HanetMemberInfo', () => {
115
117
  expect(mockedGoBack).toHaveBeenCalled();
116
118
  });
117
119
 
118
- test('Test navigate CaptureFaceID', async () => {
120
+ const chooseSetFaceIDOption = async (instance, index) => {
121
+ const option = instance.find(
122
+ (el) => el.props.testID === `${TESTID.HANET_ADD_MEMBER_OPTION}_${index}`
123
+ );
124
+ await act(async () => {
125
+ await option.props.onPress();
126
+ });
127
+ };
128
+
129
+ test('Test set photo id capture face id', async () => {
119
130
  await act(async () => {
120
131
  tree = await create(wrapComponent(route));
121
132
  });
122
133
  const instance = tree.root;
134
+ const touches = instance.findAllByType(TouchableOpacity);
135
+ expect(touches).toHaveLength(7);
123
136
 
137
+ await act(async () => {
138
+ await touches[1].props.onPress();
139
+ });
140
+
141
+ const bottomSheet = instance.findByType(BottomSheet);
142
+ expect(bottomSheet.props.isVisible).toBe(true);
143
+ await chooseSetFaceIDOption(instance, 0);
144
+
145
+ expect(bottomSheet.props.isVisible).toBe(false);
146
+ expect(mockedNavigate).toBeCalled(); // navigate to HanetCaptureFaceID
147
+ });
148
+
149
+ test('Test set photo id choose file', async () => {
150
+ await act(async () => {
151
+ tree = await create(wrapComponent(route));
152
+ });
153
+ const instance = tree.root;
124
154
  const touches = instance.findAllByType(TouchableOpacity);
125
- expect(touches).toHaveLength(5);
155
+ expect(touches).toHaveLength(7);
126
156
 
127
157
  await act(async () => {
128
158
  await touches[1].props.onPress();
129
159
  });
130
- expect(mockedNavigate).toBeCalled();
160
+
161
+ const bottomSheet = instance.findByType(BottomSheet);
162
+ expect(bottomSheet.props.isVisible).toBe(true);
163
+
164
+ axios.patch.mockImplementationOnce(async () => ({
165
+ status: 200,
166
+ data: {
167
+ avatar_uri: 'uri',
168
+ },
169
+ }));
170
+ await chooseSetFaceIDOption(instance, 1);
171
+
172
+ expect(bottomSheet.props.isVisible).toBe(false);
173
+ expect(axios.patch).toBeCalled(); // call api update face id
131
174
  });
132
175
 
133
176
  test('Test register new member', async () => {
134
177
  route.params.hanetMember = {
135
178
  alias_id: null,
136
179
  name: null,
137
- avatar_uri: 'avatar_uri',
180
+ avatar_uri: null,
138
181
  };
139
182
  route.params.isAddNewMember = true;
140
183
  await act(async () => {
141
184
  tree = await create(wrapComponent(route));
142
185
  });
143
186
  const instance = tree.root;
144
-
145
187
  const touches = instance.findAllByType(TouchableOpacity);
146
- expect(touches).toHaveLength(5);
188
+ expect(touches).toHaveLength(7);
147
189
 
148
190
  // press set face id
149
191
  await act(async () => {
150
192
  await touches[1].props.onPress();
151
193
  });
152
- expect(mockedNavigate).toBeCalled();
194
+
195
+ const bottomSheet = instance.findByType(BottomSheet);
196
+ expect(bottomSheet.props.isVisible).toBe(true);
197
+
198
+ await chooseSetFaceIDOption(instance, 1);
199
+ expect(axios.patch).not.toBeCalled(); // not call api
153
200
 
154
201
  // open alert action
155
202
  const alertAction = instance.findByType(AlertAction);
@@ -158,7 +205,7 @@ describe('Test HanetMemberInfo', () => {
158
205
  });
159
206
  expect(alertAction.props.visible).toBe(true);
160
207
 
161
- // change name and press rename
208
+ // change name and press done
162
209
  const textInput = instance.findByType(TextInput);
163
210
  await act(async () => {
164
211
  await textInput.props.onChange('new name');
@@ -0,0 +1,90 @@
1
+ import React from 'react';
2
+ import { TouchableOpacity, StyleSheet } from 'react-native';
3
+ import ImagePicker from 'react-native-image-crop-picker';
4
+ import { IconFill } from '@ant-design/icons-react-native';
5
+ import Text from '../../../commons/Text';
6
+ import BottomSheet from '../../../commons/BottomSheet';
7
+ import { Colors } from '../../../configs';
8
+ import { useTranslations } from '../../../hooks/Common/useTranslations';
9
+ import SmartPhoneSvg from '../../../../assets/images/Common/SmartPhone.svg';
10
+ import { TESTID } from '../../../configs/Constants';
11
+
12
+ const RequestFaceIDPopup = ({
13
+ title,
14
+ isVisible,
15
+ setHide,
16
+ onCaptureFaceID,
17
+ onChooseFile,
18
+ }) => {
19
+ const t = useTranslations();
20
+ const options = [
21
+ {
22
+ icon: <IconFill name="camera" color={Colors.Gray9} size={27} />,
23
+ text: t('capture_image'),
24
+ onChoose: () => {
25
+ setHide();
26
+ onCaptureFaceID && onCaptureFaceID();
27
+ },
28
+ },
29
+ {
30
+ icon: <SmartPhoneSvg />,
31
+ text: t('pick_available_image_from_your_phone'),
32
+ onChoose: async () => {
33
+ setHide();
34
+ const options = {
35
+ mediaType: 'photo',
36
+ compressImageMaxHeight: 1280,
37
+ compressImageMaxWidth: 738,
38
+ compressImageQuality: 0.7,
39
+ forceJpg: true,
40
+ };
41
+ try {
42
+ const result = await ImagePicker.openPicker(options);
43
+ onChooseFile && onChooseFile(result);
44
+ } catch (e) {}
45
+ },
46
+ },
47
+ ];
48
+
49
+ return (
50
+ <BottomSheet
51
+ isVisible={isVisible}
52
+ onBackdropPress={setHide}
53
+ title={title}
54
+ style={styles.wrap}
55
+ >
56
+ {options.map((option, i) => (
57
+ <TouchableOpacity
58
+ key={i}
59
+ onPress={option.onChoose}
60
+ style={styles.row}
61
+ testID={`${TESTID.HANET_ADD_MEMBER_OPTION}_${i}`}
62
+ >
63
+ {option.icon}
64
+ <Text type="H4" color={Colors.Gray9} style={styles.textOption}>
65
+ {option.text}
66
+ </Text>
67
+ </TouchableOpacity>
68
+ ))}
69
+ </BottomSheet>
70
+ );
71
+ };
72
+
73
+ export default RequestFaceIDPopup;
74
+
75
+ const styles = StyleSheet.create({
76
+ wrap: {
77
+ paddingBottom: 50,
78
+ },
79
+ row: {
80
+ flexDirection: 'row',
81
+ alignItems: 'center',
82
+ paddingVertical: 16,
83
+ marginHorizontal: 16,
84
+ borderBottomColor: Colors.Gray3,
85
+ borderBottomWidth: 1,
86
+ },
87
+ textOption: {
88
+ marginLeft: 18,
89
+ },
90
+ });
@@ -38,6 +38,13 @@ describe('Test useHanetCheckinData', () => {
38
38
  detected_mask: 'MASK_ON',
39
39
  created_at: moment(),
40
40
  },
41
+ {
42
+ id: 3,
43
+ person_type: 'IMAGE_FROM_CAMERA',
44
+ detected_image_uri: 'uri',
45
+ detected_mask: 'NOT_HANDLE',
46
+ created_at: moment(),
47
+ },
41
48
  ],
42
49
  };
43
50
  });
@@ -129,19 +136,9 @@ describe('Test useHanetCheckinData', () => {
129
136
  expect(result.current.countMember).toBe(1);
130
137
  expect(result.current.countStranger).toBe(1);
131
138
 
139
+ // send old data
132
140
  await act(async () => {
133
- await result.current.onChangeDate(moment().add(-1, 'days'));
134
- });
135
- const newData4 = {
136
- id: 4,
137
- person_name: 'name 2',
138
- person_type: 'STRANGER',
139
- detected_image_uri: 'uri',
140
- detected_mask: 'MASK_ON',
141
- created_at: moment(),
142
- };
143
- await act(async () => {
144
- await result.current.onReceiveNewCheckinData(newData4);
141
+ await result.current.onReceiveNewCheckinData(newData2);
145
142
  });
146
143
  // no change
147
144
  expect(result.current.checkinData).toHaveLength(2);
@@ -48,7 +48,7 @@ const useHanetCheckinData = (hanetCamera) => {
48
48
  } else {
49
49
  setCanLoadMore(page < Math.ceil(data.count / 20));
50
50
  setCheckinData((prevData) =>
51
- _.uniqBy(prevData.concat(data.results || []))
51
+ _.uniqBy(prevData.concat(data.results || []), 'id')
52
52
  );
53
53
  }
54
54
  }
@@ -80,17 +80,20 @@ const useHanetCheckinData = (hanetCamera) => {
80
80
  };
81
81
 
82
82
  const onReceiveNewCheckinData = (data) => {
83
- if (!moment(data.created_at).isSame(date, 'date')) {
83
+ if (!moment(data.created_at).isSame(moment(), 'date')) {
84
84
  return;
85
85
  }
86
86
  setCheckinData((listData) => {
87
+ const existed = listData.find((i) => i.id === data.id);
88
+ if (!existed) {
89
+ if (['EMPLOYEE', 'CUSTOMER'].includes(data.person_type)) {
90
+ setCountMember((prevData) => prevData + 1);
91
+ } else {
92
+ setCountStranger((prevData) => prevData + 1);
93
+ }
94
+ }
87
95
  return _.uniqBy([data, ...listData], 'id');
88
96
  });
89
- if (['EMPLOYEE', 'CUSTOMER'].includes(data.person_type)) {
90
- setCountMember((prevData) => prevData + 1);
91
- } else {
92
- setCountStranger((prevData) => prevData + 1);
93
- }
94
97
  };
95
98
 
96
99
  useEffect(() => {
@@ -32,18 +32,4 @@ export default StyleSheet.create({
32
32
  marginLeft: 16,
33
33
  flex: 1,
34
34
  },
35
- addMemberModal: {
36
- paddingBottom: 50,
37
- },
38
- row: {
39
- flexDirection: 'row',
40
- alignItems: 'center',
41
- paddingVertical: 16,
42
- marginHorizontal: 16,
43
- borderBottomColor: Colors.Gray3,
44
- borderBottomWidth: 1,
45
- },
46
- textOption: {
47
- marginLeft: 18,
48
- },
49
35
  });
@@ -80,7 +80,6 @@ const UnitDetail = ({ route }) => {
80
80
  const [showAdd, setShowAdd, setHideAdd] = useBoolean();
81
81
  const [isFullScreen, setIsFullScreen] = useState(false);
82
82
  const [dataFullScreen, setDataFullScreen] = useState();
83
- const [serverDown, setServerDown] = useState(false);
84
83
 
85
84
  const { childRef, showingPopover, showPopoverWithRef, hidePopover } =
86
85
  usePopover();
@@ -125,13 +124,10 @@ const UnitDetail = ({ route }) => {
125
124
  const fetchDetails = useCallback(async () => {
126
125
  getAutomates();
127
126
  await fetchWithCache(API.UNIT.UNIT_DETAIL(unitId), {}, (response) => {
128
- const { success, data, resp_status } = response;
127
+ const { success, data } = response;
129
128
  if (success) {
130
- setServerDown(false);
131
129
  prepareData(data);
132
130
  setUnit(data);
133
- } else if (resp_status >= 500) {
134
- setServerDown(true);
135
131
  }
136
132
  });
137
133
  }, [setUnit, unitId, prepareData, getAutomates]);
@@ -307,7 +303,6 @@ const UnitDetail = ({ route }) => {
307
303
  isOwner={isOwner}
308
304
  favorites={favorites}
309
305
  wrapItemStyle={styles.wrapItemStyle}
310
- serverDown={serverDown}
311
306
  isGGHomeConnected={isGGHomeConnected}
312
307
  />
313
308
  );
@@ -334,7 +329,6 @@ const UnitDetail = ({ route }) => {
334
329
  <ShortDetailSubUnit
335
330
  unit={unit}
336
331
  station={station}
337
- serverDown={serverDown}
338
332
  isGGHomeConnected={isGGHomeConnected}
339
333
  />
340
334
  );
@@ -24,17 +24,19 @@ const MyUnitDevice = ({ sensor, unit }) => {
24
24
 
25
25
  return (
26
26
  <View style={styles.item}>
27
- <TouchableOpacity style={styles.rowCenter} onPress={goToSensorDisplay}>
28
- <Image style={styles.image} source={Images.mainDoor} />
29
- <View style={styles.marginTop3}>
30
- <Text semibold style={styles.nameDevice}>
31
- {sensor.name}
32
- </Text>
33
- <View style={styles.roomDevice}>
34
- <Text style={styles.roomDevicePart}>
35
- {sensor.station_name}
36
- {status ? ` - ${status}` : ''}
27
+ <TouchableOpacity style={styles.flex1} onPress={goToSensorDisplay}>
28
+ <View style={styles.rowCenter}>
29
+ <Image style={styles.image} source={Images.mainDoor} />
30
+ <View style={styles.marginTop3}>
31
+ <Text semibold style={styles.nameDevice}>
32
+ {sensor.name}
37
33
  </Text>
34
+ <View style={styles.roomDevice}>
35
+ <Text style={styles.roomDevicePart}>
36
+ {sensor.station_name}
37
+ {status ? ` - ${status}` : ''}
38
+ </Text>
39
+ </View>
38
40
  </View>
39
41
  </View>
40
42
  </TouchableOpacity>
@@ -92,6 +94,7 @@ const styles = StyleSheet.create({
92
94
  flex: 1,
93
95
  marginTop: 3,
94
96
  },
97
+ flex1: { flex: 1 },
95
98
  });
96
99
 
97
100
  export default MyUnitDevice;
@@ -43,7 +43,7 @@ describe('Test MyUnitDevice', () => {
43
43
  });
44
44
  const instance = tree.root;
45
45
  const Views = instance.findAllByType(View);
46
- expect(Views).toHaveLength(10);
46
+ expect(Views).toHaveLength(11);
47
47
 
48
48
  const touches = instance.findAllByType(TouchableOpacity);
49
49
  expect(touches).toHaveLength(1);
@@ -60,6 +60,6 @@ describe('Test MyUnitDevice', () => {
60
60
  });
61
61
  const instance = tree.root;
62
62
  const Views = instance.findAllByType(View);
63
- expect(Views).toHaveLength(10);
63
+ expect(Views).toHaveLength(11);
64
64
  });
65
65
  });
@@ -938,6 +938,7 @@
938
938
  "mask_off": "Mask off",
939
939
  "mask_on": "Mask on",
940
940
  "stranger": "Stranger",
941
+ "image_from_camera": "Image from camera",
941
942
  "icon_unit": "Icon unit",
942
943
  "lock": "LOCK",
943
944
  "unlock": "UNLOCK",
@@ -940,6 +940,7 @@
940
940
  "mask_off": "Không có khẩu trang",
941
941
  "mask_on": "Có khẩu trang",
942
942
  "stranger": "Người lạ",
943
+ "image_from_camera": "Ảnh chụp",
943
944
  "icon_unit": "Ảnh đại diện địa điểm",
944
945
  "lock": "KHÓA",
945
946
  "unlock": "MỞ KHÓA",