agora-appbuilder-core 4.1.8-beta.6 → 4.1.8-beta.8

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agora-appbuilder-core",
3
- "version": "4.1.8-beta.6",
3
+ "version": "4.1.8-beta.8",
4
4
  "description": "React Native template for RTE app builder",
5
5
  "main": "index.js",
6
6
  "files": [
@@ -77,8 +77,8 @@ const DefaultConfig = {
77
77
  CHAT_ORG_NAME: '',
78
78
  CHAT_APP_NAME: '',
79
79
  CHAT_URL: '',
80
- CLI_VERSION: '3.1.8-beta.6',
81
- CORE_VERSION: '4.1.8-beta.6',
80
+ CLI_VERSION: '3.1.8-beta.8',
81
+ CORE_VERSION: '4.1.8-beta.8',
82
82
  DISABLE_LANDSCAPE_MODE: false,
83
83
  STT_AUTO_START: false,
84
84
  CLOUD_RECORDING_AUTO_START: false,
@@ -89,7 +89,8 @@ const DefaultConfig = {
89
89
  AI_LAYOUT: 'LAYOUT_TYPE_1',
90
90
  SDK_CODEC: 'vp8',
91
91
  ENABLE_WAITING_ROOM_AUTO_APPROVAL: false,
92
- ENABLE_WAITING_ROOM_AUTO_REQUEST: false
92
+ ENABLE_WAITING_ROOM_AUTO_REQUEST: false,
93
+ ENABLE_TEXT_TRACKS: true,
93
94
  };
94
95
 
95
96
  module.exports = DefaultConfig;
@@ -177,6 +177,7 @@ interface ConfigInterface {
177
177
  SDK_CODEC: string;
178
178
  ENABLE_WAITING_ROOM_AUTO_APPROVAL: boolean;
179
179
  ENABLE_WAITING_ROOM_AUTO_REQUEST: boolean;
180
+ ENABLE_TEXT_TRACKS: boolean;
180
181
  }
181
182
  declare var $config: ConfigInterface;
182
183
  declare module 'customization' {
@@ -64,7 +64,7 @@
64
64
  "agora-extension-beauty-effect": "^1.0.2-beta",
65
65
  "agora-extension-virtual-background": "^1.1.3",
66
66
  "agora-react-native-rtm": "1.5.1",
67
- "agora-rtc-sdk-ng": "4.23.3",
67
+ "agora-rtc-sdk-ng": "4.23.4",
68
68
  "agora-rtm-sdk": "1.5.1",
69
69
  "buffer": "^6.0.3",
70
70
  "electron-log": "4.3.5",
@@ -19,6 +19,7 @@ interface TableHeaderProps {
19
19
  rowStyle?: ViewStyle;
20
20
  cellStyle?: ViewStyle;
21
21
  firstCellStyle?: ViewStyle;
22
+ lastCellStyle?: ViewStyle;
22
23
  textStyle?: TextStyle;
23
24
  }
24
25
 
@@ -28,22 +29,27 @@ const TableHeader: React.FC<TableHeaderProps> = ({
28
29
  rowStyle,
29
30
  cellStyle,
30
31
  firstCellStyle,
32
+ lastCellStyle,
31
33
  textStyle,
32
34
  }) => (
33
35
  <View style={[style.thead, containerStyle]}>
34
36
  <View style={[style.throw, rowStyle]}>
35
- {columns.map((col, index) => (
36
- <View
37
- key={col}
38
- style={[
39
- style.th,
40
- index === 0 && style.plzero,
41
- cellStyle,
42
- index === 0 && firstCellStyle,
43
- ]}>
44
- <Text style={[style.thText, textStyle]}>{col}</Text>
45
- </View>
46
- ))}
37
+ {columns.map((col, index) => {
38
+ const isFirst = index === 0;
39
+ const isLast = index === (columns.length > 1 ? columns.length - 1 : 0);
40
+ return (
41
+ <View
42
+ key={col}
43
+ style={[
44
+ style.th,
45
+ cellStyle,
46
+ isFirst && firstCellStyle,
47
+ isLast && lastCellStyle,
48
+ ]}>
49
+ <Text style={[style.thText, textStyle]}>{col}</Text>
50
+ </View>
51
+ );
52
+ })}
47
53
  </View>
48
54
  </View>
49
55
  );
@@ -151,7 +157,7 @@ const TableFooter: React.FC<TableFooterProps> = ({
151
157
 
152
158
  export {TableHeader, TableFooter, TableBody};
153
159
 
154
- const style = StyleSheet.create({
160
+ export const style = StyleSheet.create({
155
161
  scrollgrow: {
156
162
  flexGrow: 1,
157
163
  },
@@ -222,7 +228,7 @@ const style = StyleSheet.create({
222
228
  flex: 1,
223
229
  alignSelf: 'stretch',
224
230
  justifyContent: 'center',
225
- paddingHorizontal: 12,
231
+ // paddingHorizontal: 12,
226
232
  },
227
233
  thText: {
228
234
  color: $config.FONT_COLOR + ThemeConfig.EmphasisPlus.medium,
@@ -249,7 +255,6 @@ const style = StyleSheet.create({
249
255
  flex: 1,
250
256
  alignSelf: 'center',
251
257
  justifyContent: 'center',
252
- // height: 100,
253
258
  gap: 10,
254
259
  },
255
260
  tpreview: {
@@ -275,6 +280,8 @@ const style = StyleSheet.create({
275
280
  tactions: {
276
281
  display: 'flex',
277
282
  flexDirection: 'row',
283
+ alignItems: 'center',
284
+ justifyContent: 'flex-end',
278
285
  },
279
286
  tlink: {
280
287
  color: $config.PRIMARY_ACTION_BRAND_COLOR,
@@ -382,4 +389,24 @@ const style = StyleSheet.create({
382
389
  pl15: {
383
390
  paddingLeft: 15,
384
391
  },
392
+ // icon celles
393
+ tdIconCell: {
394
+ flex: 0,
395
+ flexShrink: 0,
396
+ alignItems: 'flex-start',
397
+ justifyContent: 'center',
398
+ minWidth: 52,
399
+ // paddingRight: 50 + 12,
400
+ },
401
+ thIconCell: {
402
+ flex: 0,
403
+ flexShrink: 0,
404
+ alignSelf: 'stretch',
405
+ justifyContent: 'center',
406
+ minWidth: 50,
407
+ paddingHorizontal: 12,
408
+ },
409
+ alignCellToRight: {
410
+ alignItems: 'flex-end',
411
+ },
385
412
  });
@@ -40,6 +40,7 @@ export const controlPermissionMatrix: Record<
40
40
  isHost &&
41
41
  $config.ENABLE_STT &&
42
42
  $config.ENABLE_MEETING_TRANSCRIPT &&
43
+ $config.ENABLE_TEXT_TRACKS &&
43
44
  isWeb(),
44
45
  };
45
46
 
@@ -0,0 +1,285 @@
1
+ import React, {useEffect, useState} from 'react';
2
+ import {View, Text, Linking, TouchableOpacity, StyleSheet} from 'react-native';
3
+ import {downloadRecording, getDuration, getRecordedDateTime} from './utils';
4
+ import IconButtonWithToolTip from '../../atoms/IconButton';
5
+ import Tooltip from '../../atoms/Tooltip';
6
+ import Clipboard from '../../subComponents/Clipboard';
7
+ import Spacer from '../../atoms/Spacer';
8
+ import PlatformWrapper from '../../utils/PlatformWrapper';
9
+ import {useFetchSTTTranscript} from '../text-tracks/useFetchSTTTranscript';
10
+ import {style} from '../common/data-table';
11
+ import {FetchRecordingData} from '../../subComponents/recording/useRecording';
12
+ import ImageIcon from '../../atoms/ImageIcon';
13
+ import TextTrackItemRow from './TextTrackItemRow';
14
+
15
+ interface RecordingItemRowProps {
16
+ item: FetchRecordingData['recordings'][0];
17
+ onDeleteAction: (id: string) => void;
18
+ onTextTrackDownload: (textTrackLink: string) => void;
19
+ }
20
+ export default function RecordingItemRow({
21
+ item,
22
+ onDeleteAction,
23
+ onTextTrackDownload,
24
+ }: RecordingItemRowProps) {
25
+ const [expanded, setIsExpanded] = useState(false);
26
+
27
+ const [date, time] = getRecordedDateTime(item.created_at);
28
+ const recordingStatus = item.status;
29
+
30
+ const {sttRecState, getSTTsForRecording} = useFetchSTTTranscript();
31
+ const {
32
+ status,
33
+ error,
34
+ data: {stts = []},
35
+ } = sttRecState;
36
+
37
+ useEffect(() => {
38
+ if (expanded) {
39
+ if (item.id) {
40
+ getSTTsForRecording(item.id);
41
+ }
42
+ }
43
+ }, [expanded, item.id, getSTTsForRecording]);
44
+
45
+ if (
46
+ recordingStatus === 'STOPPING' ||
47
+ recordingStatus === 'STARTED' ||
48
+ (recordingStatus === 'INPROGRESS' && !item?.download_url)
49
+ ) {
50
+ return (
51
+ <View key={item.id} style={style.pt12}>
52
+ <View style={[style.infotextContainer, style.captionContainer]}>
53
+ <ImageIcon
54
+ iconSize={20}
55
+ iconType="plain"
56
+ name="info"
57
+ tintColor={$config.SEMANTIC_NEUTRAL}
58
+ />
59
+ <Text style={[style.captionText]}>
60
+ Current recording is ongoing. Once it concludes, we'll generate the
61
+ link
62
+ </Text>
63
+ </View>
64
+ </View>
65
+ );
66
+ }
67
+
68
+ // Collapsible Row
69
+ return (
70
+ <View>
71
+ {/* ========== PARENT ROW ========== */}
72
+ <View style={style.tbrow} key={item.id}>
73
+ <View style={style.tdIconCell}>
74
+ <IconButtonWithToolTip
75
+ hoverEffect={true}
76
+ hoverEffectStyle={style.iconButtonHoverEffect}
77
+ containerStyle={style.iconButton}
78
+ iconProps={{
79
+ name: expanded ? 'arrow-up' : 'arrow-down',
80
+ iconType: 'plain',
81
+ iconSize: 20,
82
+ tintColor: `${$config.FONT_COLOR}`,
83
+ }}
84
+ onPress={() => setIsExpanded(prev => !prev)}
85
+ />
86
+ </View>
87
+ <View style={[style.td, style.plzero]}>
88
+ <Text style={style.ttime}>
89
+ {date}
90
+ <br />
91
+ <Text style={style.ttime}>{time}</Text>
92
+ </Text>
93
+ </View>
94
+ <View style={[style.td]}>
95
+ <Text style={style.ttime}>
96
+ {getDuration(item.created_at, item.ended_at)}
97
+ </Text>
98
+ </View>
99
+ <View style={style.td}>
100
+ {!item.download_url ? (
101
+ <View style={(style.tactions, {marginTop: 0})}>
102
+ <Text style={style.placeHolder}>{'No recording found'}</Text>
103
+ </View>
104
+ ) : item?.download_url?.length > 0 ? (
105
+ <View style={style.tactions}>
106
+ <View>
107
+ {item?.download_url?.map((link: string, i: number) => (
108
+ <View
109
+ style={[
110
+ style.tactions,
111
+ //if recording contains multiple parts then we need to add some space each row
112
+ i >= 1 ? {marginTop: 8} : {},
113
+ ]}>
114
+ <View>
115
+ <IconButtonWithToolTip
116
+ hoverEffect={true}
117
+ hoverEffectStyle={style.iconButtonHoverEffect}
118
+ containerStyle={style.iconButton}
119
+ iconProps={{
120
+ name: 'download',
121
+ iconType: 'plain',
122
+ iconSize: 20,
123
+ tintColor: `${$config.SECONDARY_ACTION_COLOR}`,
124
+ }}
125
+ onPress={() => {
126
+ downloadRecording(link);
127
+ }}
128
+ />
129
+ </View>
130
+ <View style={style.pl10}>
131
+ <IconButtonWithToolTip
132
+ hoverEffect={true}
133
+ hoverEffectStyle={style.iconButtonHoverEffect}
134
+ containerStyle={style.iconButton}
135
+ iconProps={{
136
+ name: 'link-share',
137
+ iconType: 'plain',
138
+ iconSize: 20,
139
+ tintColor: `${$config.SECONDARY_ACTION_COLOR}`,
140
+ }}
141
+ onPress={async () => {
142
+ if (await Linking.canOpenURL(link)) {
143
+ await Linking.openURL(link);
144
+ }
145
+ }}
146
+ />
147
+ </View>
148
+ <View style={[style.pl10]}>
149
+ <Tooltip
150
+ isClickable
151
+ placement="left"
152
+ toolTipMessage="Link Copied"
153
+ onPress={() => {
154
+ Clipboard.setString(link);
155
+ }}
156
+ toolTipIcon={
157
+ <>
158
+ <ImageIcon
159
+ iconType="plain"
160
+ name="tick-fill"
161
+ tintColor={$config.SEMANTIC_SUCCESS}
162
+ iconSize={20}
163
+ />
164
+ <Spacer size={8} horizontal={true} />
165
+ </>
166
+ }
167
+ fontSize={12}
168
+ renderContent={() => {
169
+ return (
170
+ <PlatformWrapper>
171
+ {(isHovered: boolean) => (
172
+ <TouchableOpacity
173
+ style={[
174
+ isHovered
175
+ ? style.iconButtonHoverEffect
176
+ : {},
177
+ style.iconShareLink,
178
+ ]}
179
+ onPress={() => {
180
+ Clipboard.setString(link);
181
+ }}>
182
+ <ImageIcon
183
+ iconType="plain"
184
+ name="copy-link"
185
+ iconSize={20}
186
+ tintColor={$config.SECONDARY_ACTION_COLOR}
187
+ />
188
+ </TouchableOpacity>
189
+ )}
190
+ </PlatformWrapper>
191
+ );
192
+ }}
193
+ />
194
+ </View>
195
+ </View>
196
+ ))}
197
+ </View>
198
+ <View style={[style.pl10]}>
199
+ <IconButtonWithToolTip
200
+ hoverEffect={true}
201
+ hoverEffectStyle={style.iconButtonHoverEffect}
202
+ containerStyle={style.iconButton}
203
+ iconProps={{
204
+ name: 'delete',
205
+ iconType: 'plain',
206
+ iconSize: 20,
207
+ tintColor: `${$config.SEMANTIC_ERROR}`,
208
+ }}
209
+ onPress={() => {
210
+ onDeleteAction && onDeleteAction(item.id);
211
+ }}
212
+ />
213
+ </View>
214
+ </View>
215
+ ) : (
216
+ <View style={(style.tactions, {marginTop: 0})}>
217
+ <Text style={style.placeHolder}>No recordings found</Text>
218
+ </View>
219
+ )}
220
+ </View>
221
+ </View>
222
+ {/* ========== CHILDREN ROW ========== */}
223
+ {expanded && (
224
+ <View style={expanedStyles.expandedContainer}>
225
+ <View>
226
+ <Text style={expanedStyles.expandedHeaderText}>Text-tracks</Text>
227
+ </View>
228
+ <View style={expanedStyles.expandedHeaderBody}>
229
+ {status === 'idle' || status === 'pending' ? (
230
+ <Text style={style.ttime}>Fetching text-tracks....</Text>
231
+ ) : status === 'rejected' ? (
232
+ <Text style={style.ttime}>
233
+ {error?.message ||
234
+ 'There was an error while fetching the text-tracks'}
235
+ </Text>
236
+ ) : status === 'resolved' && stts?.length === 0 ? (
237
+ <Text style={style.ttime}>
238
+ There are no text-tracks's for this recording
239
+ </Text>
240
+ ) : (
241
+ <>
242
+ <Text style={style.ttime}>Found {stts.length} text tracks</Text>
243
+ <View>
244
+ {stts.map(item => (
245
+ <TextTrackItemRow
246
+ key={item.id}
247
+ item={item}
248
+ onTextTrackDownload={onTextTrackDownload}
249
+ />
250
+ ))}
251
+ </View>
252
+ </>
253
+ )}
254
+ </View>
255
+ </View>
256
+ )}
257
+ </View>
258
+ );
259
+ }
260
+
261
+ const expanedStyles = StyleSheet.create({
262
+ expandedContainer: {
263
+ display: 'flex',
264
+ flexDirection: 'column',
265
+ gap: 5,
266
+ color: $config.FONT_COLOR,
267
+ borderColor: $config.CARD_LAYER_3_COLOR,
268
+ backgroundColor: $config.CARD_LAYER_2_COLOR,
269
+ paddingHorizontal: 12,
270
+ paddingVertical: 15,
271
+ borderRadius: 5,
272
+ },
273
+ expandedHeaderText: {
274
+ fontSize: 15,
275
+ lineHeight: 32,
276
+ fontWeight: '500',
277
+ color: $config.FONT_COLOR,
278
+ },
279
+ expandedHeaderBody: {
280
+ display: 'flex',
281
+ flexDirection: 'row',
282
+ justifyContent: 'space-between',
283
+ alignItems: 'flex-start',
284
+ },
285
+ });
@@ -1,30 +1,65 @@
1
1
  import React, {useState, useEffect} from 'react';
2
2
  import {View, Text} from 'react-native';
3
- import {style} from './style';
4
- import {RTableHeader, RTableBody, RTableFooter} from './recording-table';
5
- import {useRecording} from '../../subComponents/recording/useRecording';
3
+ import {
4
+ APIStatus,
5
+ FetchRecordingData,
6
+ useRecording,
7
+ } from '../../subComponents/recording/useRecording';
6
8
  import events from '../../rtm-events-api';
7
9
  import {EventNames} from '../../rtm-events';
10
+ import {style, TableBody, TableHeader} from '../common/data-table';
11
+ import Loading from '../../subComponents/Loading';
12
+ import ImageIcon from '../../atoms/ImageIcon';
13
+ import RecordingItemRow from './RecordingItemRow';
14
+ import GenericPopup from '../common/GenericPopup';
15
+ import {downloadS3Link} from '../../utils/common';
16
+
17
+ function EmptyRecordingState() {
18
+ return (
19
+ <View style={style.infotextContainer}>
20
+ <View>
21
+ <ImageIcon
22
+ iconType="plain"
23
+ name="info"
24
+ tintColor={'#777777'}
25
+ iconSize={32}
26
+ />
27
+ </View>
28
+ <View>
29
+ <Text style={[style.infoText, style.pt10, style.pl10]}>
30
+ No recording found for this meeting
31
+ </Text>
32
+ </View>
33
+ </View>
34
+ );
35
+ }
36
+
37
+ const headers = ['', 'Date/Time', 'Duration', 'Actions'];
38
+ const defaultPageNumber = 1;
8
39
 
9
40
  function RecordingsDateTable(props) {
10
- const [state, setState] = React.useState({
41
+ const [state, setState] = React.useState<{
42
+ status: APIStatus;
43
+ data: {
44
+ recordings: FetchRecordingData['recordings'];
45
+ pagination: FetchRecordingData['pagination'];
46
+ };
47
+ error: Error;
48
+ }>({
11
49
  status: 'idle',
12
50
  data: {
13
- pagination: {},
14
51
  recordings: [],
52
+ pagination: {total: 0, limit: 10, page: defaultPageNumber},
15
53
  },
16
54
  error: null,
17
55
  });
18
- const {
19
- status,
20
- data: {pagination, recordings},
21
- error,
22
- } = state;
56
+
57
+ const [currentPage, setCurrentPage] = useState(defaultPageNumber);
23
58
 
24
59
  const {fetchRecordings} = useRecording();
25
60
 
26
- const defaultPageNumber = 1;
27
- const [currentPage, setCurrentPage] = useState(defaultPageNumber);
61
+ // message for any download‐error popup
62
+ const [errorSnack, setErrorSnack] = React.useState<string | undefined>();
28
63
 
29
64
  const onRecordingDeleteCallback = () => {
30
65
  setCurrentPage(defaultPageNumber);
@@ -38,7 +73,7 @@ function RecordingsDateTable(props) {
38
73
  };
39
74
  }, []);
40
75
 
41
- const getRecordings = pageNumber => {
76
+ const getRecordings = (pageNumber: number) => {
42
77
  setState(prev => ({...prev, status: 'pending'}));
43
78
  fetchRecordings(pageNumber).then(
44
79
  response =>
@@ -47,8 +82,13 @@ function RecordingsDateTable(props) {
47
82
  status: 'resolved',
48
83
  data: {
49
84
  recordings: response?.recordings || [],
50
- pagination: response?.pagination || {},
85
+ pagination: response?.pagination || {
86
+ total: 0,
87
+ limit: 10,
88
+ page: defaultPageNumber,
89
+ },
51
90
  },
91
+ error: null,
52
92
  })),
53
93
  error => setState(prev => ({...prev, status: 'rejected', error})),
54
94
  );
@@ -58,26 +98,53 @@ function RecordingsDateTable(props) {
58
98
  getRecordings(currentPage);
59
99
  }, [currentPage]);
60
100
 
61
- if (status === 'rejected') {
101
+ if (state.status === 'rejected') {
62
102
  return (
63
103
  <Text style={[style.ttime, style.pv10, style.ph20]}>
64
- {error?.message}
104
+ {state.error?.message}
65
105
  </Text>
66
106
  );
67
107
  }
108
+ const onTextTrackDownload = (textTrackLink: string) => {
109
+ downloadS3Link(textTrackLink).catch((err: Error) => {
110
+ setErrorSnack(err.message || 'Download failed');
111
+ });
112
+ };
113
+
68
114
  return (
69
115
  <View style={style.ttable}>
70
- <RTableHeader />
71
- <RTableBody
72
- status={status}
73
- recordings={recordings}
74
- onDeleteAction={props?.onDeleteAction}
116
+ <TableHeader
117
+ columns={headers}
118
+ firstCellStyle={style.thIconCell}
119
+ lastCellStyle={style.alignCellToRight}
75
120
  />
76
- <RTableFooter
77
- currentPage={currentPage}
78
- setCurrentPage={setCurrentPage}
79
- pagination={pagination}
121
+ <TableBody
122
+ status={state.status}
123
+ items={state.data.recordings}
124
+ loadingComponent={
125
+ <Loading background="transparent" text="Fetching recordingss.." />
126
+ }
127
+ renderRow={item => (
128
+ <RecordingItemRow
129
+ key={item.id}
130
+ item={item}
131
+ onDeleteAction={props?.onDeleteAction}
132
+ onTextTrackDownload={onTextTrackDownload}
133
+ />
134
+ )}
135
+ emptyComponent={<EmptyRecordingState />}
80
136
  />
137
+ {/** ERROR POPUP **/}
138
+ {errorSnack && (
139
+ <GenericPopup
140
+ title="Error"
141
+ variant="error"
142
+ message={errorSnack}
143
+ visible={true}
144
+ setVisible={() => setErrorSnack(undefined)}
145
+ onConfirm={() => setErrorSnack(undefined)}
146
+ />
147
+ )}
81
148
  </View>
82
149
  );
83
150
  }
@@ -0,0 +1,120 @@
1
+ import React from 'react';
2
+ import {View, Text, TouchableOpacity} from 'react-native';
3
+ import IconButtonWithToolTip from '../../atoms/IconButton';
4
+ import Tooltip from '../../atoms/Tooltip';
5
+ import Clipboard from '../../subComponents/Clipboard';
6
+ import Spacer from '../../atoms/Spacer';
7
+ import PlatformWrapper from '../../utils/PlatformWrapper';
8
+ import {FetchSTTTranscriptResponse} from '../text-tracks/useFetchSTTTranscript';
9
+ import {style} from '../common/data-table';
10
+ import ImageIcon from '../../atoms/ImageIcon';
11
+
12
+ interface TextTrackItemRowProps {
13
+ item: FetchSTTTranscriptResponse['stts'][0];
14
+ onTextTrackDownload: (link: string) => void;
15
+ }
16
+
17
+ export default function TextTrackItemRow({
18
+ item,
19
+ onTextTrackDownload,
20
+ }: TextTrackItemRowProps) {
21
+ const textTrackStatus = item.status;
22
+
23
+ return (
24
+ <View style={style.td} key={item.id}>
25
+ {!item.download_url ? (
26
+ <View style={[style.tactions, {marginTop: 0}]}>
27
+ {textTrackStatus === 'STOPPING' ||
28
+ textTrackStatus === 'STARTED' ||
29
+ (textTrackStatus === 'INPROGRESS' && !item?.download_url) ? (
30
+ <Text style={style.placeHolder}>
31
+ {'The link will be generated once the meeting ends'}
32
+ </Text>
33
+ ) : (
34
+ <Text style={style.placeHolder}>{'No text-tracks found'}</Text>
35
+ )}
36
+ </View>
37
+ ) : item?.download_url?.length > 0 ? (
38
+ <View style={style.tactions}>
39
+ <View>
40
+ {item?.download_url?.map((link: string, i: number) => (
41
+ <View
42
+ key={i}
43
+ style={[
44
+ style.tactions,
45
+ //if stts contains multiple parts then we need to add some space each row
46
+ i >= 1 ? {marginTop: 8} : {},
47
+ ]}>
48
+ <View>
49
+ <IconButtonWithToolTip
50
+ hoverEffect={true}
51
+ hoverEffectStyle={style.iconButtonHoverEffect}
52
+ containerStyle={style.iconButton}
53
+ iconProps={{
54
+ name: 'download',
55
+ iconType: 'plain',
56
+ iconSize: 20,
57
+ tintColor: `${$config.SECONDARY_ACTION_COLOR}`,
58
+ }}
59
+ onPress={() => {
60
+ onTextTrackDownload && onTextTrackDownload(link);
61
+ }}
62
+ />
63
+ </View>
64
+ <View style={[style.pl10]}>
65
+ <Tooltip
66
+ isClickable
67
+ placement="left"
68
+ toolTipMessage="Link Copied"
69
+ onPress={() => {
70
+ Clipboard.setString(link);
71
+ }}
72
+ toolTipIcon={
73
+ <>
74
+ <ImageIcon
75
+ iconType="plain"
76
+ name="tick-fill"
77
+ tintColor={$config.SEMANTIC_SUCCESS}
78
+ iconSize={20}
79
+ />
80
+ <Spacer size={8} horizontal={true} />
81
+ </>
82
+ }
83
+ fontSize={12}
84
+ renderContent={() => {
85
+ return (
86
+ <PlatformWrapper>
87
+ {(isHovered: boolean) => (
88
+ <TouchableOpacity
89
+ style={[
90
+ isHovered ? style.iconButtonHoverEffect : {},
91
+ style.iconShareLink,
92
+ ]}
93
+ onPress={() => {
94
+ Clipboard.setString(link);
95
+ }}>
96
+ <ImageIcon
97
+ iconType="plain"
98
+ name="copy-link"
99
+ iconSize={20}
100
+ tintColor={$config.SECONDARY_ACTION_COLOR}
101
+ />
102
+ </TouchableOpacity>
103
+ )}
104
+ </PlatformWrapper>
105
+ );
106
+ }}
107
+ />
108
+ </View>
109
+ </View>
110
+ ))}
111
+ </View>
112
+ </View>
113
+ ) : (
114
+ <View style={(style.tactions, {marginTop: 0})}>
115
+ <Text style={style.placeHolder}>No text-tracks found</Text>
116
+ </View>
117
+ )}
118
+ </View>
119
+ );
120
+ }
@@ -204,16 +204,14 @@ function ErrorTextTrackState({message}: {message: string}) {
204
204
  }
205
205
 
206
206
  function TextTracksTable() {
207
+ const {sttState, currentPage, setCurrentPage, deleteTranscript} =
208
+ useFetchSTTTranscript();
209
+
207
210
  const {
208
211
  status,
209
- stts,
210
- pagination,
212
+ data: {stts, pagination},
211
213
  error: fetchTranscriptError,
212
- currentPage,
213
- setCurrentPage,
214
- deleteTranscript,
215
- } = useFetchSTTTranscript();
216
-
214
+ } = sttState;
217
215
  // id of text-tracj to delete
218
216
  const [textTrackIdToDelete, setTextTrackIdToDelete] = React.useState<
219
217
  string | undefined
@@ -5,11 +5,7 @@ import getUniqueID from '../../utils/getUniqueID';
5
5
  import {logger, LogSource} from '../../logger/AppBuilderLogger';
6
6
 
7
7
  export interface FetchSTTTranscriptResponse {
8
- pagination: {
9
- limit: number;
10
- total: number;
11
- page: number;
12
- };
8
+ pagination: {limit: number; total: number; page: number};
13
9
  stts: {
14
10
  id: string;
15
11
  download_url: string[];
@@ -23,118 +19,114 @@ export interface FetchSTTTranscriptResponse {
23
19
 
24
20
  export type APIStatus = 'idle' | 'pending' | 'resolved' | 'rejected';
25
21
 
26
- export function useFetchSTTTranscript(defaultLimit = 10) {
22
+ export function useFetchSTTTranscript() {
27
23
  const {
28
24
  data: {roomId},
29
25
  } = useRoomInfo();
30
26
  const {store} = useContext(StorageContext);
27
+
31
28
  const [currentPage, setCurrentPage] = useState(1);
32
29
 
33
- const [state, setState] = useState<{
30
+ const [sttState, setSttState] = useState<{
34
31
  status: APIStatus;
35
32
  data: {
36
33
  stts: FetchSTTTranscriptResponse['stts'];
37
34
  pagination: FetchSTTTranscriptResponse['pagination'];
38
35
  };
39
- error: Error;
36
+ error: Error | null;
40
37
  }>({
41
38
  status: 'idle',
42
- data: {stts: [], pagination: {total: 0, limit: defaultLimit, page: 1}},
39
+ data: {stts: [], pagination: {total: 0, limit: 10, page: 1}},
43
40
  error: null,
44
41
  });
45
42
 
46
- const fetchStts = useCallback(
47
- async (page: number) => {
48
- const requestId = getUniqueID();
49
- const start = Date.now();
43
+ //–– by‐recording state ––
44
+ const [sttRecState, setSttRecState] = useState<{
45
+ status: APIStatus;
46
+ data: {stts: FetchSTTTranscriptResponse['stts']};
47
+ error: Error | null;
48
+ }>({
49
+ status: 'idle',
50
+ data: {
51
+ stts: [],
52
+ },
53
+ error: null,
54
+ });
50
55
 
51
- try {
52
- if (!roomId?.host) {
53
- const error = new Error('room id is empty');
54
- return Promise.reject(error);
55
- }
56
- const res = await fetch(
57
- `${$config.BACKEND_ENDPOINT}/v1/stt-transcript`,
58
- {
59
- method: 'POST',
60
- headers: {
61
- 'Content-Type': 'application/json',
62
- authorization: store.token ? `Bearer ${store.token}` : '',
63
- 'X-Request-Id': requestId,
64
- 'X-Session-Id': logger.getSessionId(),
65
- },
66
- body: JSON.stringify({
67
- passphrase: roomId.host,
68
- limit: defaultLimit,
69
- page,
70
- }),
71
- },
72
- );
73
- const json = await res.json();
74
- const end = Date.now();
56
+ const getSTTs = useCallback(
57
+ (page: number) => {
58
+ setSttState(s => ({...s, status: 'pending', error: null}));
59
+ const reqId = getUniqueID();
60
+ const start = Date.now();
75
61
 
76
- if (!res.ok) {
77
- logger.error(
62
+ fetch(`${$config.BACKEND_ENDPOINT}/v1/stt-transcript`, {
63
+ method: 'POST',
64
+ headers: {
65
+ 'Content-Type': 'application/json',
66
+ authorization: store.token ? `Bearer ${store.token}` : '',
67
+ 'X-Request-Id': reqId,
68
+ 'X-Session-Id': logger.getSessionId(),
69
+ },
70
+ body: JSON.stringify({
71
+ passphrase: roomId.host,
72
+ limit: 10,
73
+ page,
74
+ }),
75
+ })
76
+ .then(async res => {
77
+ const json = await res.json();
78
+ const end = Date.now();
79
+ if (!res.ok) {
80
+ logger.error(
81
+ LogSource.NetworkRest,
82
+ 'stt-transcript',
83
+ 'Fetch STT transcripts failed',
84
+ {
85
+ json,
86
+ start,
87
+ end,
88
+ latency: end - start,
89
+ requestId: reqId,
90
+ },
91
+ );
92
+ throw new Error(json?.error?.message || res.statusText);
93
+ }
94
+ logger.debug(
78
95
  LogSource.NetworkRest,
79
96
  'stt-transcript',
80
- 'Fetching STT transcripts failed',
97
+ 'Fetch STT transcripts succeeded',
81
98
  {
82
99
  json,
83
100
  start,
84
101
  end,
85
102
  latency: end - start,
86
- requestId,
103
+ requestId: reqId,
87
104
  },
88
105
  );
89
- throw new Error(json?.error?.message || 'Unknown fetch error');
90
- }
91
-
92
- logger.debug(
93
- LogSource.NetworkRest,
94
- 'stt-transcript',
95
- 'Fetched STT transcripts',
96
- {
97
- json,
98
- start,
99
- end,
100
- latency: end - start,
101
- requestId,
102
- },
103
- );
104
- return json;
105
- } catch (err) {
106
- return Promise.reject(err);
107
- }
108
- },
109
- [roomId.host, store.token, defaultLimit],
110
- );
111
-
112
- const getSTTs = useCallback(
113
- (page: number) => {
114
- setState(s => ({...s, status: 'pending'}));
115
- fetchStts(page).then(
116
- data =>
117
- setState({
106
+ return json as FetchSTTTranscriptResponse;
107
+ })
108
+ .then(({stts = [], pagination = {total: 0, limit: 10, page}}) => {
109
+ setSttState({
118
110
  status: 'resolved',
119
- data: {
120
- stts: data.stts || [],
121
- pagination: data.pagination || {
122
- total: 0,
123
- limit: defaultLimit,
124
- page: 1,
125
- },
126
- },
111
+ data: {stts, pagination},
127
112
  error: null,
128
- }),
129
- err => setState(s => ({...s, status: 'rejected', error: err})),
130
- );
113
+ });
114
+ })
115
+ .catch(err => {
116
+ setSttState(s => ({...s, status: 'rejected', error: err}));
117
+ });
131
118
  },
132
- [fetchStts, defaultLimit],
119
+ [roomId.host, store.token],
133
120
  );
134
121
 
122
+ useEffect(() => {
123
+ getSTTs(currentPage);
124
+ }, [currentPage, getSTTs]);
125
+
126
+ // Delete stts
135
127
  const deleteTranscript = useCallback(
136
128
  async (id: string) => {
137
- const requestId = getUniqueID();
129
+ const reqId = getUniqueID();
138
130
  const start = Date.now();
139
131
 
140
132
  const res = await fetch(
@@ -148,7 +140,7 @@ export function useFetchSTTTranscript(defaultLimit = 10) {
148
140
  headers: {
149
141
  'Content-Type': 'application/json',
150
142
  authorization: store.token ? `Bearer ${store.token}` : '',
151
- 'X-Request-Id': requestId,
143
+ 'X-Request-Id': reqId,
152
144
  'X-Session-Id': logger.getSessionId(),
153
145
  },
154
146
  },
@@ -159,44 +151,33 @@ export function useFetchSTTTranscript(defaultLimit = 10) {
159
151
  logger.error(
160
152
  LogSource.NetworkRest,
161
153
  'stt-transcript',
162
- 'Deleting STT transcripts failed',
163
- {
164
- json: '',
165
- start,
166
- end,
167
- latency: end - start,
168
- requestId,
169
- },
154
+ 'Delete transcript failed',
155
+ {start, end, latency: end - start, requestId: reqId},
170
156
  );
171
157
  throw new Error(`Delete failed (${res.status})`);
172
158
  }
173
159
  logger.debug(
174
160
  LogSource.NetworkRest,
175
161
  'stt-transcript',
176
- 'Deleted STT transcripts',
177
- {
178
- json: '',
179
- start,
180
- end,
181
- latency: end - start,
182
- requestId,
183
- },
162
+ 'Delete transcript succeeded',
163
+ {start, end, latency: end - start, requestId: reqId},
184
164
  );
185
- // optimistic update local state:
186
- setState(prev => {
165
+
166
+ // optimistic remove from paginated list
167
+ setSttState(prev => {
187
168
  // remove the deleted item
188
169
  const newStts = prev.data.stts.filter(item => item.id !== id);
189
170
  // decrement total count
190
171
  const newTotal = Math.max(prev.data.pagination.total - 1, 0);
191
- // if we just removed the *last* item on this page, go back a page
192
172
  let newPage = prev.data.pagination.page;
193
173
  if (prev.data.stts.length === 1 && newPage > 1) {
194
- newPage = newPage - 1;
174
+ newPage--;
195
175
  }
196
176
  return {
197
177
  ...prev,
198
178
  data: {
199
179
  stts: newStts,
180
+
200
181
  pagination: {
201
182
  ...prev.data.pagination,
202
183
  total: newTotal,
@@ -206,20 +187,80 @@ export function useFetchSTTTranscript(defaultLimit = 10) {
206
187
  };
207
188
  });
208
189
  },
209
- [roomId.host, store?.token],
190
+ [roomId.host, store.token],
210
191
  );
211
192
 
212
- useEffect(() => {
213
- getSTTs(currentPage);
214
- }, [currentPage, getSTTs]);
193
+ //–– fetch for a given recording ––
194
+ const getSTTsForRecording = useCallback(
195
+ (recordingId: string) => {
196
+ setSttRecState(r => ({...r, status: 'pending', error: null}));
197
+ const reqId = getUniqueID();
198
+ const start = Date.now();
199
+
200
+ fetch(`${$config.BACKEND_ENDPOINT}/v1/recording/stt-transcript`, {
201
+ method: 'POST',
202
+ headers: {
203
+ 'Content-Type': 'application/json',
204
+ authorization: store.token ? `Bearer ${store.token}` : '',
205
+ 'X-Request-Id': reqId,
206
+ 'X-Session-Id': logger.getSessionId(),
207
+ },
208
+ body: JSON.stringify({
209
+ project_id: $config.PROJECT_ID,
210
+ recording_id: recordingId,
211
+ }),
212
+ })
213
+ .then(async res => {
214
+ const json = await res.json();
215
+ const end = Date.now();
216
+ console.log('supriua json', json);
217
+ if (!res.ok) {
218
+ logger.error(
219
+ LogSource.NetworkRest,
220
+ 'stt-transcript',
221
+ 'Fetch stt-by-recording failed',
222
+ {json, start, end, latency: end - start, requestId: reqId},
223
+ );
224
+ throw new Error(json?.error?.message || res.statusText);
225
+ }
226
+ logger.debug(
227
+ LogSource.NetworkRest,
228
+ 'stt-transcript',
229
+ 'Fetch stt-by-recording succeeded',
230
+ {json, start, end, latency: end - start, requestId: reqId},
231
+ );
232
+ if (json?.error) {
233
+ logger.debug(
234
+ LogSource.NetworkRest,
235
+ 'stt-transcript',
236
+ `No STT records found (code ${json.error.code}): ${json.error.message}`,
237
+ {start, end, latency: end - start, reqId},
238
+ );
239
+ return [];
240
+ } else {
241
+ return json as FetchSTTTranscriptResponse['stts'];
242
+ }
243
+ })
244
+ .then(stts =>
245
+ setSttRecState({status: 'resolved', data: {stts}, error: null}),
246
+ )
247
+ .catch(err =>
248
+ setSttRecState(r => ({...r, status: 'rejected', error: err})),
249
+ );
250
+ },
251
+ [store.token],
252
+ );
215
253
 
216
254
  return {
217
- status: state.status as APIStatus,
218
- stts: state.data.stts,
219
- pagination: state.data.pagination,
220
- error: state.error,
255
+ // stt list
256
+ sttState,
257
+ getSTTs,
221
258
  currentPage,
222
259
  setCurrentPage,
260
+ // STT per recording
261
+ sttRecState,
262
+ getSTTsForRecording,
263
+ // delete
223
264
  deleteTranscript,
224
265
  };
225
266
  }
@@ -67,16 +67,31 @@ const getFrontendUrl = (url: string) => {
67
67
  return url;
68
68
  };
69
69
 
70
- interface RecordingsData {
71
- recordings: [];
72
- pagination: {};
70
+ export type APIStatus = 'idle' | 'pending' | 'resolved' | 'rejected';
71
+
72
+ export interface FetchRecordingData {
73
+ pagination: {
74
+ limit: number;
75
+ total: number;
76
+ page: number;
77
+ };
78
+ recordings: {
79
+ id: string;
80
+ download_url: string[];
81
+ title: string;
82
+ product_name: string;
83
+ status: 'COMPLETED' | 'STARTED' | 'INPROGRESS' | 'STOPPING';
84
+ created_at: string;
85
+ ended_at: string;
86
+ }[];
73
87
  }
88
+
74
89
  export interface RecordingContextInterface {
75
90
  startRecording: () => void;
76
91
  stopRecording: () => void;
77
92
  isRecordingActive: boolean;
78
93
  inProgress: boolean;
79
- fetchRecordings?: (page: number) => Promise<RecordingsData>;
94
+ fetchRecordings?: (page: number) => Promise<FetchRecordingData>;
80
95
  deleteRecording?: (id: number) => Promise<boolean>;
81
96
  }
82
97