agora-appbuilder-core 4.1.8-beta.1 → 4.1.8-beta.11

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 (26) hide show
  1. package/package.json +2 -2
  2. package/template/agora-rn-uikit/src/Contexts/PropsContext.tsx +1 -1
  3. package/template/agora-rn-uikit/src/Rtc/Join.tsx +18 -9
  4. package/template/bridge/rtc/webNg/RtcEngine.ts +2 -2
  5. package/template/defaultConfig.js +4 -3
  6. package/template/global.d.ts +1 -0
  7. package/template/package.json +1 -1
  8. package/template/src/atoms/TextInput.tsx +3 -0
  9. package/template/src/components/Controls.tsx +17 -15
  10. package/template/src/components/common/GenericPopup.tsx +0 -1
  11. package/template/src/components/common/data-table.tsx +42 -15
  12. package/template/src/components/controls/useControlPermissionMatrix.tsx +7 -4
  13. package/template/src/components/recordings/RecordingItemRow.tsx +289 -0
  14. package/template/src/components/recordings/RecordingsDateTable.tsx +99 -25
  15. package/template/src/components/recordings/TextTrackItemRow.tsx +120 -0
  16. package/template/src/components/room-info/useRoomInfo.tsx +1 -0
  17. package/template/src/components/{stt-transcript/STTTranscriptTable.tsx → text-tracks/TextTracksTable.tsx} +61 -50
  18. package/template/src/components/{stt-transcript/ViewSTTTranscriptModal.tsx → text-tracks/ViewTextTracksModal.tsx} +7 -7
  19. package/template/src/components/text-tracks/useFetchSTTTranscript.tsx +262 -0
  20. package/template/src/language/default-labels/videoCallScreenLabels.ts +7 -7
  21. package/template/src/pages/VideoCall.tsx +1 -1
  22. package/template/src/subComponents/ChatInput.tsx +72 -1
  23. package/template/src/subComponents/recording/useRecording.tsx +19 -4
  24. package/template/src/utils/useCreateRoom.ts +1 -1
  25. package/template/src/utils/useJoinRoom.ts +5 -1
  26. package/template/src/components/stt-transcript/useFetchSTTTranscript.tsx +0 -193
@@ -0,0 +1,262 @@
1
+ import {useState, useCallback, useEffect, useContext} from 'react';
2
+ import StorageContext from '../StorageContext';
3
+ import {useRoomInfo} from 'customization-api';
4
+ import getUniqueID from '../../utils/getUniqueID';
5
+ import {logger, LogSource} from '../../logger/AppBuilderLogger';
6
+
7
+ export interface FetchSTTTranscriptResponse {
8
+ pagination: {limit: number; total: number; page: number};
9
+ stts: {
10
+ id: string;
11
+ download_url: string[];
12
+ title: string;
13
+ product_name: string;
14
+ status: 'COMPLETED' | 'STARTED' | 'INPROGRESS' | 'STOPPING';
15
+ created_at: string;
16
+ ended_at: string;
17
+ }[];
18
+ }
19
+
20
+ export type APIStatus = 'idle' | 'pending' | 'resolved' | 'rejected';
21
+
22
+ export function useFetchSTTTranscript() {
23
+ const {
24
+ data: {roomId},
25
+ } = useRoomInfo();
26
+ const {store} = useContext(StorageContext);
27
+
28
+ const [currentPage, setCurrentPage] = useState(1);
29
+
30
+ const [sttState, setSttState] = useState<{
31
+ status: APIStatus;
32
+ data: {
33
+ stts: FetchSTTTranscriptResponse['stts'];
34
+ pagination: FetchSTTTranscriptResponse['pagination'];
35
+ };
36
+ error: Error | null;
37
+ }>({
38
+ status: 'idle',
39
+ data: {stts: [], pagination: {total: 0, limit: 10, page: 1}},
40
+ error: null,
41
+ });
42
+
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
+ });
55
+
56
+ const getSTTs = useCallback(
57
+ (page: number) => {
58
+ setSttState(s => ({...s, status: 'pending', error: null}));
59
+ const reqId = getUniqueID();
60
+ const start = Date.now();
61
+
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(
95
+ LogSource.NetworkRest,
96
+ 'stt-transcript',
97
+ 'Fetch STT transcripts succeeded',
98
+ {
99
+ json,
100
+ start,
101
+ end,
102
+ latency: end - start,
103
+ requestId: reqId,
104
+ },
105
+ );
106
+ return json as FetchSTTTranscriptResponse;
107
+ })
108
+ .then(({stts = [], pagination = {total: 0, limit: 10, page}}) => {
109
+ setSttState({
110
+ status: 'resolved',
111
+ data: {stts, pagination},
112
+ error: null,
113
+ });
114
+ })
115
+ .catch(err => {
116
+ setSttState(s => ({...s, status: 'rejected', error: err}));
117
+ });
118
+ },
119
+ [roomId.host, store.token],
120
+ );
121
+
122
+ // Delete stts
123
+ const deleteTranscript = useCallback(
124
+ async (id: string) => {
125
+ const reqId = getUniqueID();
126
+ const start = Date.now();
127
+
128
+ const res = await fetch(
129
+ `${
130
+ $config.BACKEND_ENDPOINT
131
+ }/v1/stt-transcript/${id}?passphrase=${encodeURIComponent(
132
+ roomId.host,
133
+ )}`,
134
+ {
135
+ method: 'DELETE',
136
+ headers: {
137
+ 'Content-Type': 'application/json',
138
+ authorization: store.token ? `Bearer ${store.token}` : '',
139
+ 'X-Request-Id': reqId,
140
+ 'X-Session-Id': logger.getSessionId(),
141
+ },
142
+ },
143
+ );
144
+ const end = Date.now();
145
+
146
+ if (!res.ok) {
147
+ logger.error(
148
+ LogSource.NetworkRest,
149
+ 'stt-transcript',
150
+ 'Delete transcript failed',
151
+ {start, end, latency: end - start, requestId: reqId},
152
+ );
153
+ throw new Error(`Delete failed (${res.status})`);
154
+ }
155
+ logger.debug(
156
+ LogSource.NetworkRest,
157
+ 'stt-transcript',
158
+ 'Delete transcript succeeded',
159
+ {start, end, latency: end - start, requestId: reqId},
160
+ );
161
+
162
+ // optimistic remove from paginated list
163
+ setSttState(prev => {
164
+ // remove the deleted item
165
+ const newStts = prev.data.stts.filter(item => item.id !== id);
166
+ // decrement total count
167
+ const newTotal = Math.max(prev.data.pagination.total - 1, 0);
168
+ let newPage = prev.data.pagination.page;
169
+ if (prev.data.stts.length === 1 && newPage > 1) {
170
+ newPage--;
171
+ }
172
+ return {
173
+ ...prev,
174
+ data: {
175
+ stts: newStts,
176
+
177
+ pagination: {
178
+ ...prev.data.pagination,
179
+ total: newTotal,
180
+ page: newPage,
181
+ },
182
+ },
183
+ };
184
+ });
185
+ },
186
+ [roomId.host, store.token],
187
+ );
188
+
189
+ //–– fetch for a given recording ––
190
+ const getSTTsForRecording = useCallback(
191
+ (recordingId: string) => {
192
+ setSttRecState(r => ({...r, status: 'pending', error: null}));
193
+ const reqId = getUniqueID();
194
+ const start = Date.now();
195
+
196
+ fetch(`${$config.BACKEND_ENDPOINT}/v1/recording/stt-transcript`, {
197
+ method: 'POST',
198
+ headers: {
199
+ 'Content-Type': 'application/json',
200
+ authorization: store.token ? `Bearer ${store.token}` : '',
201
+ 'X-Request-Id': reqId,
202
+ 'X-Session-Id': logger.getSessionId(),
203
+ },
204
+ body: JSON.stringify({
205
+ project_id: $config.PROJECT_ID,
206
+ recording_id: recordingId,
207
+ }),
208
+ })
209
+ .then(async res => {
210
+ const json = await res.json();
211
+ const end = Date.now();
212
+ console.log('supriua json', json);
213
+ if (!res.ok) {
214
+ logger.error(
215
+ LogSource.NetworkRest,
216
+ 'stt-transcript',
217
+ 'Fetch stt-by-recording failed',
218
+ {json, start, end, latency: end - start, requestId: reqId},
219
+ );
220
+ throw new Error(json?.error?.message || res.statusText);
221
+ }
222
+ logger.debug(
223
+ LogSource.NetworkRest,
224
+ 'stt-transcript',
225
+ 'Fetch stt-by-recording succeeded',
226
+ {json, start, end, latency: end - start, requestId: reqId},
227
+ );
228
+ if (json?.error) {
229
+ logger.debug(
230
+ LogSource.NetworkRest,
231
+ 'stt-transcript',
232
+ `No STT records found (code ${json.error.code}): ${json.error.message}`,
233
+ {start, end, latency: end - start, reqId},
234
+ );
235
+ return [];
236
+ } else {
237
+ return json as FetchSTTTranscriptResponse['stts'];
238
+ }
239
+ })
240
+ .then(stts =>
241
+ setSttRecState({status: 'resolved', data: {stts}, error: null}),
242
+ )
243
+ .catch(err =>
244
+ setSttRecState(r => ({...r, status: 'rejected', error: err})),
245
+ );
246
+ },
247
+ [store.token],
248
+ );
249
+
250
+ return {
251
+ // stt list
252
+ sttState,
253
+ getSTTs,
254
+ currentPage,
255
+ setCurrentPage,
256
+ // STT per recording
257
+ sttRecState,
258
+ getSTTsForRecording,
259
+ // delete
260
+ deleteTranscript,
261
+ };
262
+ }
@@ -109,8 +109,8 @@ export const toolbarItemNoiseCancellationText =
109
109
  export const toolbarItemWhiteboardText = 'toolbarItemWhiteboardText';
110
110
  export const toolbarItemCaptionText = 'toolbarItemCaptionText';
111
111
  export const toolbarItemTranscriptText = 'toolbarItemTranscriptText';
112
- export const toolbarItemManageTranscriptText =
113
- 'toolbarItemManageTranscriptText';
112
+ export const toolbarItemManageTextTracksText =
113
+ 'toolbarItemManageTextTracksText';
114
114
  export const toolbarItemVirtualBackgroundText =
115
115
  'toolbarItemVirtualBackgroundText';
116
116
  export const toolbarItemViewRecordingText = 'toolbarItemViewRecordingText';
@@ -151,7 +151,7 @@ export const nativeStopScreensharePopupPrimaryBtnText =
151
151
  'nativeStopScreensharePopupPrimaryBtnText';
152
152
 
153
153
  export const recordingModalTitleIntn = 'recordingModalTitleIntn';
154
- export const transcriptModalTitleIntn = 'transcriptModalTitleIntn';
154
+ export const textTrackModalTitleIntn = 'textTrackModalTitleIntn';
155
155
  export const stopRecordingPopupHeading = 'stopRecordingPopupHeading';
156
156
  export const stopRecordingPopupSubHeading = 'stopRecordingPopupSubHeading';
157
157
  export const stopRecordingPopupPrimaryBtnText =
@@ -573,7 +573,7 @@ export interface I18nVideoCallScreenLabelsInterface {
573
573
  [toolbarItemWhiteboardText]?: I18nConditionalType;
574
574
  [toolbarItemCaptionText]?: I18nConditionalType;
575
575
  [toolbarItemTranscriptText]?: I18nConditionalType;
576
- [toolbarItemManageTranscriptText]?: I18nConditionalType;
576
+ [toolbarItemManageTextTracksText]?: I18nConditionalType;
577
577
  [toolbarItemVirtualBackgroundText]?: I18nBaseType;
578
578
  [toolbarItemViewRecordingText]?: I18nConditionalType;
579
579
 
@@ -609,7 +609,7 @@ export interface I18nVideoCallScreenLabelsInterface {
609
609
  [nativeStopScreensharePopupPrimaryBtnText]?: I18nBaseType;
610
610
 
611
611
  [recordingModalTitleIntn]?: I18nBaseType;
612
- [transcriptModalTitleIntn]?: I18nBaseType;
612
+ [textTrackModalTitleIntn]?: I18nBaseType;
613
613
  [stopRecordingPopupHeading]?: I18nBaseType;
614
614
  [stopRecordingPopupSubHeading]?: I18nBaseType;
615
615
  [stopRecordingPopupPrimaryBtnText]?: I18nBaseType;
@@ -942,7 +942,7 @@ export const VideoCallScreenLabels: I18nVideoCallScreenLabelsInterface = {
942
942
  [toolbarItemTranscriptText]: active =>
943
943
  active ? 'Hide Transcript' : 'Show Transcript',
944
944
  [toolbarItemViewRecordingText]: 'View Recordings',
945
- [toolbarItemManageTranscriptText]: 'View Transcripts',
945
+ [toolbarItemManageTextTracksText]: 'View Text-tracks',
946
946
 
947
947
  [toolbarItemRaiseHandText]: active => (active ? 'Lower Hand' : 'Raise Hand'),
948
948
  [toolbarItemSwitchCameraText]: 'Switch Camera',
@@ -1025,7 +1025,7 @@ export const VideoCallScreenLabels: I18nVideoCallScreenLabelsInterface = {
1025
1025
  `Once removed, ${name} will still be able to screen share later.`,
1026
1026
  [removeScreenshareFromRoomPopupPrimaryBtnText]: 'REMOVE',
1027
1027
 
1028
- [transcriptModalTitleIntn]: 'View Transcripts',
1028
+ [textTrackModalTitleIntn]: 'Text Tracks',
1029
1029
  [sttChangeLanguagePopupHeading]: isFirstTimeOpened =>
1030
1030
  isFirstTimeOpened ? 'Set Spoken Language' : 'Change Spoken Language',
1031
1031
  [sttChangeLanguagePopupSubHeading]:
@@ -369,7 +369,7 @@ const VideoCall: React.FC = () => {
369
369
  encryption: $config.ENCRYPTION_ENABLED
370
370
  ? {
371
371
  key: data.encryptionSecret,
372
- mode: RnEncryptionEnum.AES256GCM2,
372
+ mode: data.encryptionMode,
373
373
  screenKey: data.encryptionSecret,
374
374
  salt: data.encryptionSecretSalt,
375
375
  }
@@ -97,6 +97,10 @@ export const ChatTextInput = (props: ChatTextInputProps) => {
97
97
  replyToMsgId,
98
98
  setReplyToMsgId,
99
99
  } = useChatUIControls();
100
+
101
+ // Track IME composition state
102
+ const [isComposing, setIsComposing] = React.useState(false);
103
+
100
104
  const {defaultContent} = useContent();
101
105
  const {sendChatSDKMessage, uploadAttachment} = useChatConfigure();
102
106
  const {addMessageToPrivateStore, addMessageToStore} = useChatMessages();
@@ -115,6 +119,41 @@ export const ChatTextInput = (props: ChatTextInputProps) => {
115
119
  });
116
120
  }, []);
117
121
 
122
+ // Set up direct DOM event listeners for IME composition
123
+ useEffect(() => {
124
+ if (!isWeb()) return;
125
+
126
+ const inputElement = chatInputRef?.current;
127
+ if (!inputElement) return;
128
+
129
+ // Get the actual DOM element (React Native Web creates a textarea/input)
130
+ const domElement = inputElement._nativeTag
131
+ ? document.querySelector(`[data-tag="${inputElement._nativeTag}"]`)
132
+ : inputElement;
133
+
134
+ if (!domElement) return;
135
+
136
+ const handleCompositionStart = () => {
137
+ setIsComposing(true);
138
+ };
139
+
140
+ const handleCompositionEnd = () => {
141
+ setIsComposing(false);
142
+ };
143
+
144
+ // Add event listeners directly to DOM element
145
+ domElement.addEventListener('compositionstart', handleCompositionStart);
146
+ domElement.addEventListener('compositionend', handleCompositionEnd);
147
+
148
+ return () => {
149
+ domElement.removeEventListener(
150
+ 'compositionstart',
151
+ handleCompositionStart,
152
+ );
153
+ domElement.removeEventListener('compositionend', handleCompositionEnd);
154
+ };
155
+ }, [chatInputRef?.current]);
156
+
118
157
  const {data} = useRoomInfo();
119
158
  const [name] = useUserName();
120
159
  const toastHeadingSize = useString(chatSendErrorTextSizeToastHeading)();
@@ -166,9 +205,37 @@ export const ChatTextInput = (props: ChatTextInputProps) => {
166
205
  });
167
206
  };
168
207
 
208
+ // IME composition handlers
209
+ const handleCompositionStart = () => {
210
+ setIsComposing(true);
211
+ };
212
+
213
+ const handleCompositionEnd = () => {
214
+ setIsComposing(false);
215
+ };
216
+
217
+ const handleInput = event => {
218
+ // Reset composition state if input event occurs without active composition
219
+ if (isWeb() && !event.nativeEvent.isComposing && isComposing) {
220
+ setIsComposing(false);
221
+ }
222
+ };
223
+
169
224
  // with multiline textinput enter prints /n
170
225
  const handleKeyPress = ({nativeEvent}) => {
171
- if (nativeEvent.key === 'Enter' && !nativeEvent.shiftKey) {
226
+ const currentlyComposing = nativeEvent.isComposing || isComposing;
227
+
228
+ // Check if this is an Enter key during composition
229
+ if (nativeEvent.key === 'Enter' && currentlyComposing) {
230
+ return;
231
+ }
232
+
233
+ // Only submit on Enter if not composing with IME and no Shift key
234
+ if (
235
+ nativeEvent.key === 'Enter' &&
236
+ !nativeEvent.shiftKey &&
237
+ !currentlyComposing
238
+ ) {
172
239
  nativeEvent.preventDefault();
173
240
  onSubmitEditing();
174
241
  setShowEmojiPicker(false); // This will close emoji picker on enter
@@ -225,6 +292,10 @@ export const ChatTextInput = (props: ChatTextInputProps) => {
225
292
  autoCorrect={false}
226
293
  onKeyPress={handleKeyPress}
227
294
  onChange={_handleHeightChange}
295
+ // IME composition event handlers for React Native Web
296
+ onCompositionStart={isWeb() ? handleCompositionStart : undefined}
297
+ onCompositionEnd={isWeb() ? handleCompositionEnd : undefined}
298
+ onInput={isWeb() ? handleInput : undefined}
228
299
  />
229
300
  );
230
301
 
@@ -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
 
@@ -41,7 +41,7 @@ export default function useCreateRoom(): createRoomFun {
41
41
  try {
42
42
  const payload = JSON.stringify({
43
43
  title: roomTitle,
44
- enablePSTN: enablePSTN,
44
+ enable_pstn: enablePSTN,
45
45
  });
46
46
  const response = await fetch(`${CREATE_ROOM_URL}`, {
47
47
  method: 'POST',
@@ -58,7 +58,6 @@ export default function useJoinRoom() {
58
58
  send_event: false,
59
59
  });
60
60
  } else {
61
- console.log('debugging store.token', store.token);
62
61
  logger.log(
63
62
  LogSource.NetworkRest,
64
63
  'joinChannel',
@@ -157,6 +156,11 @@ export default function useJoinRoom() {
157
156
  isWaitingRoomEnabled ? data.secretSalt : data.secret_salt,
158
157
  ) as Uint8Array;
159
158
  }
159
+
160
+ if (data?.encryption_mode) {
161
+ roomInfo.encryptionMode = data.encryption_mode;
162
+ }
163
+
160
164
  if (data?.screen_share_user?.uid || data?.screenShare?.uid) {
161
165
  roomInfo.screenShareUid = isWaitingRoomEnabled
162
166
  ? data.screenShare.uid
@@ -1,193 +0,0 @@
1
- import {useState, useCallback, useEffect, useContext} from 'react';
2
- import StorageContext from '../StorageContext';
3
- import {useRoomInfo} from 'customization-api';
4
- import getUniqueID from '../../utils/getUniqueID';
5
- import {logger, LogSource} from '../../logger/AppBuilderLogger';
6
-
7
- export interface FetchSTTTranscriptResponse {
8
- pagination: {
9
- limit: number;
10
- total: number;
11
- page: number;
12
- };
13
- stts: {
14
- id: string;
15
- download_url: string[];
16
- title: string;
17
- product_name: string;
18
- status: 'COMPLETED' | 'STARTED' | 'INPROGRESS' | 'STOPPING';
19
- created_at: string;
20
- ended_at: string;
21
- }[];
22
- }
23
-
24
- export type APIStatus = 'idle' | 'pending' | 'resolved' | 'rejected';
25
-
26
- export function useFetchSTTTranscript(defaultLimit = 10) {
27
- const {
28
- data: {roomId},
29
- } = useRoomInfo();
30
- const {store} = useContext(StorageContext);
31
- const [currentPage, setCurrentPage] = useState(1);
32
-
33
- const [state, setState] = useState<{
34
- status: APIStatus;
35
- data: {
36
- stts: FetchSTTTranscriptResponse['stts'];
37
- pagination: FetchSTTTranscriptResponse['pagination'];
38
- };
39
- error: any;
40
- }>({
41
- status: 'idle',
42
- data: {stts: [], pagination: {total: 0, limit: defaultLimit, page: 1}},
43
- error: null,
44
- });
45
-
46
- const fetchStts = useCallback(
47
- async (page: number) => {
48
- const requestId = getUniqueID();
49
- const start = Date.now();
50
-
51
- try {
52
- if (!roomId?.host) {
53
- return Promise.reject('room id is empty');
54
- }
55
- const res = await fetch(
56
- `${$config.BACKEND_ENDPOINT}/v1/stt-transcript`,
57
- {
58
- method: 'POST',
59
- headers: {
60
- 'Content-Type': 'application/json',
61
- authorization: store.token ? `Bearer ${store.token}` : '',
62
- 'X-Request-Id': requestId,
63
- 'X-Session-Id': logger.getSessionId(),
64
- },
65
- body: JSON.stringify({
66
- passphrase: roomId.host,
67
- limit: defaultLimit,
68
- page,
69
- }),
70
- },
71
- );
72
- const json = await res.json();
73
- const end = Date.now();
74
-
75
- if (!res.ok) {
76
- logger.error(
77
- LogSource.NetworkRest,
78
- 'stt-transcript',
79
- 'Fetching STT transcripts failed',
80
- {
81
- json,
82
- start,
83
- end,
84
- latency: end - start,
85
- requestId,
86
- },
87
- );
88
- throw new Error(json?.error?.message || 'Unknown fetch error');
89
- }
90
-
91
- logger.debug(
92
- LogSource.NetworkRest,
93
- 'stt-transcript',
94
- 'Fetched STT transcripts',
95
- {
96
- json,
97
- start,
98
- end,
99
- latency: end - start,
100
- requestId,
101
- },
102
- );
103
- return json;
104
- } catch (err) {
105
- return Promise.reject(err);
106
- }
107
- },
108
- [roomId.host, store.token, defaultLimit],
109
- );
110
-
111
- const getSTTs = useCallback(
112
- (page: number) => {
113
- setState(s => ({...s, status: 'pending'}));
114
- fetchStts(page).then(
115
- data =>
116
- setState({
117
- status: 'resolved',
118
- data: {
119
- stts: data.stts || [],
120
- pagination: data.pagination || {
121
- total: 0,
122
- limit: defaultLimit,
123
- page: 1,
124
- },
125
- },
126
- error: null,
127
- }),
128
- err => setState(s => ({...s, status: 'rejected', error: err})),
129
- );
130
- },
131
- [fetchStts, defaultLimit],
132
- );
133
-
134
- const deleteTranscript = useCallback(
135
- async (id: string) => {
136
- const res = await fetch(
137
- `${
138
- $config.BACKEND_ENDPOINT
139
- }/v1/stt-transcript/${id}?passphrase=${encodeURIComponent(
140
- roomId.host,
141
- )}`,
142
- {
143
- method: 'DELETE',
144
- headers: {'Content-Type': 'application/json'},
145
- },
146
- );
147
- if (!res.ok) {
148
- const body = await res.json();
149
- throw new Error(
150
- body?.error?.message ?? `Delete failed (${res.status})`,
151
- );
152
- }
153
- // optimistic update local state:
154
- setState(prev => {
155
- // remove the deleted item
156
- const newStts = prev.data.stts.filter(item => item.id !== id);
157
- // decrement total count
158
- const newTotal = Math.max(prev.data.pagination.total - 1, 0);
159
- // if we just removed the *last* item on this page, go back a page
160
- let newPage = prev.data.pagination.page;
161
- if (prev.data.stts.length === 1 && newPage > 1) {
162
- newPage = newPage - 1;
163
- }
164
- return {
165
- ...prev,
166
- data: {
167
- stts: newStts,
168
- pagination: {
169
- ...prev.data.pagination,
170
- total: newTotal,
171
- page: newPage,
172
- },
173
- },
174
- };
175
- });
176
- },
177
- [roomId.host],
178
- );
179
-
180
- useEffect(() => {
181
- getSTTs(currentPage);
182
- }, [currentPage, getSTTs]);
183
-
184
- return {
185
- status: state.status as APIStatus,
186
- stts: state.data.stts,
187
- pagination: state.data.pagination,
188
- error: state.error,
189
- currentPage,
190
- setCurrentPage,
191
- deleteTranscript,
192
- };
193
- }