@selfcommunity/react-core 0.6.7-alpha.8 → 0.6.7-payments.143
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/lib/cjs/components/provider/SCUserProvider/index.js +6 -0
- package/lib/cjs/constants/Cache.d.ts +21 -2
- package/lib/cjs/constants/Cache.js +29 -4
- package/lib/cjs/constants/Preferences.d.ts +13 -0
- package/lib/cjs/constants/Preferences.js +24 -1
- package/lib/cjs/constants/Routes.d.ts +8 -0
- package/lib/cjs/constants/Routes.js +17 -1
- package/lib/cjs/hooks/useSCFetchCategories.d.ts +2 -0
- package/lib/cjs/hooks/useSCFetchCategories.js +7 -4
- package/lib/cjs/hooks/useSCFetchCourse.d.ts +23 -0
- package/lib/cjs/hooks/useSCFetchCourse.js +81 -0
- package/lib/cjs/hooks/useSCFetchCourses.d.ts +22 -0
- package/lib/cjs/hooks/useSCFetchCourses.js +83 -0
- package/lib/cjs/hooks/useSCFetchLesson.d.ts +22 -0
- package/lib/cjs/hooks/useSCFetchLesson.js +72 -0
- package/lib/cjs/hooks/useSCFetchLessonCommentObject.d.ts +23 -0
- package/lib/cjs/hooks/useSCFetchLessonCommentObject.js +72 -0
- package/lib/cjs/hooks/useSCFetchLessonCommentObjects.d.ts +48 -0
- package/lib/cjs/hooks/useSCFetchLessonCommentObjects.js +302 -0
- package/lib/cjs/hooks/useSCFetchUserBlockedBy.js +1 -1
- package/lib/cjs/hooks/useSCJoinedCoursesManager.d.ts +38 -0
- package/lib/cjs/hooks/useSCJoinedCoursesManager.js +277 -0
- package/lib/cjs/index.d.ts +7 -2
- package/lib/cjs/index.js +12 -2
- package/lib/cjs/themes/theme.js +12 -0
- package/lib/cjs/types/context.d.ts +36 -1
- package/lib/cjs/types/index.d.ts +2 -2
- package/lib/cjs/types/theme.d.ts +52 -0
- package/lib/esm/components/provider/SCUserProvider/index.js +6 -0
- package/lib/esm/constants/Cache.d.ts +21 -2
- package/lib/esm/constants/Cache.js +21 -2
- package/lib/esm/constants/Preferences.d.ts +13 -0
- package/lib/esm/constants/Preferences.js +23 -0
- package/lib/esm/constants/Routes.d.ts +8 -0
- package/lib/esm/constants/Routes.js +16 -0
- package/lib/esm/hooks/useSCFetchCategories.d.ts +2 -0
- package/lib/esm/hooks/useSCFetchCategories.js +7 -4
- package/lib/esm/hooks/useSCFetchCourse.d.ts +23 -0
- package/lib/esm/hooks/useSCFetchCourse.js +78 -0
- package/lib/esm/hooks/useSCFetchCourses.d.ts +22 -0
- package/lib/esm/hooks/useSCFetchCourses.js +81 -0
- package/lib/esm/hooks/useSCFetchLesson.d.ts +22 -0
- package/lib/esm/hooks/useSCFetchLesson.js +69 -0
- package/lib/esm/hooks/useSCFetchLessonCommentObject.d.ts +23 -0
- package/lib/esm/hooks/useSCFetchLessonCommentObject.js +69 -0
- package/lib/esm/hooks/useSCFetchLessonCommentObjects.d.ts +48 -0
- package/lib/esm/hooks/useSCFetchLessonCommentObjects.js +297 -0
- package/lib/esm/hooks/useSCFetchUserBlockedBy.js +1 -1
- package/lib/esm/hooks/useSCJoinedCoursesManager.d.ts +38 -0
- package/lib/esm/hooks/useSCJoinedCoursesManager.js +273 -0
- package/lib/esm/index.d.ts +7 -2
- package/lib/esm/index.js +6 -1
- package/lib/esm/themes/theme.js +12 -0
- package/lib/esm/types/context.d.ts +36 -1
- package/lib/esm/types/index.d.ts +2 -2
- package/lib/esm/types/theme.d.ts +52 -0
- package/lib/umd/react-core.js +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { useEffect, useReducer } from 'react';
|
|
2
|
+
import { SCOPE_SC_CORE } from '../constants/Errors';
|
|
3
|
+
import { SCCommentsOrderBy } from '@selfcommunity/types';
|
|
4
|
+
import { Endpoints, http } from '@selfcommunity/api-services';
|
|
5
|
+
import { CacheStrategies, Logger, LRUCache } from '@selfcommunity/utils';
|
|
6
|
+
import { getLessonCommentCacheKey, getLessonCommentsCacheKey } from '../constants/Cache';
|
|
7
|
+
import { useIsComponentMountedRef } from '../utils/hooks';
|
|
8
|
+
import { getCurrentPage } from '../utils/pagination';
|
|
9
|
+
import useSCFetchLesson from '../hooks/useSCFetchLesson';
|
|
10
|
+
/**
|
|
11
|
+
* @hidden
|
|
12
|
+
* We have complex state logic that involves multiple sub-values,
|
|
13
|
+
* so useReducer is preferable to useState.
|
|
14
|
+
* Define all possible auth action types label
|
|
15
|
+
* Use this to export actions and dispatch an action
|
|
16
|
+
*/
|
|
17
|
+
export const commentsObjectActionTypes = {
|
|
18
|
+
LOADING_NEXT: '_loading_next',
|
|
19
|
+
LOADING_PREVIOUS: '_loading_previous',
|
|
20
|
+
DATA_NEXT_LOADED: '_data_next_loaded',
|
|
21
|
+
DATA_PREVIOUS_LOADED: '_data_previous_loaded',
|
|
22
|
+
DATA_RELOAD: '_data_reload',
|
|
23
|
+
DATA_RELOADED: '_data_reloaded',
|
|
24
|
+
DATA_REVALIDATE: '_data_revalidate',
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* commentsReducer:
|
|
28
|
+
* - manage the state of comments object
|
|
29
|
+
* - update the state base on action type
|
|
30
|
+
* @param state
|
|
31
|
+
* @param action
|
|
32
|
+
*/
|
|
33
|
+
function commentsReducer(state, action) {
|
|
34
|
+
switch (action.type) {
|
|
35
|
+
case commentsObjectActionTypes.LOADING_NEXT:
|
|
36
|
+
return Object.assign(Object.assign({}, state), { isLoadingNext: true, isLoadingPrevious: false });
|
|
37
|
+
case commentsObjectActionTypes.LOADING_PREVIOUS:
|
|
38
|
+
return Object.assign(Object.assign({}, state), { isLoadingNext: false, isLoadingPrevious: true });
|
|
39
|
+
case commentsObjectActionTypes.DATA_NEXT_LOADED:
|
|
40
|
+
return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, state), { comments: action.payload.comments, isLoadingNext: false, componentLoaded: true, revalidate: false, revalidateNext: null, revalidatePrevious: null, next: action.payload.next }), (action.payload.page ? { page: action.payload.page } : {})), (action.payload.nextPage ? { nextPage: action.payload.nextPage } : {})), (action.payload.previous ? { previous: action.payload.previous } : {})), (action.payload.previousPage ? { previousPage: action.payload.previousPage } : {})), (action.payload.total ? { total: action.payload.total } : {}));
|
|
41
|
+
case commentsObjectActionTypes.DATA_PREVIOUS_LOADED:
|
|
42
|
+
return Object.assign(Object.assign(Object.assign(Object.assign({}, state), { comments: action.payload.comments, isLoadingPrevious: false, revalidate: false, revalidateNext: null, revalidatePrevious: null, previous: action.payload.previous }), (action.payload.page ? { page: action.payload.page } : {})), (action.payload.previousPage ? { previousPage: action.payload.previousPage } : {}));
|
|
43
|
+
case commentsObjectActionTypes.DATA_RELOAD:
|
|
44
|
+
return Object.assign(Object.assign({}, state), { next: action.payload.next, previousPage: null, nextPage: null, comments: [], total: 0, previous: null, isLoadingNext: true, reload: true });
|
|
45
|
+
case commentsObjectActionTypes.DATA_RELOADED:
|
|
46
|
+
return Object.assign(Object.assign({}, state), { componentLoaded: true, reload: false });
|
|
47
|
+
case commentsObjectActionTypes.DATA_REVALIDATE:
|
|
48
|
+
return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, state), { componentLoaded: true, revalidate: true }), (action.payload.revalidateNext ? { revalidateNext: action.payload.revalidateNext } : {})), (action.payload.revalidatePrevious ? { revalidatePrevious: action.payload.revalidatePrevious } : {})), { reload: false });
|
|
49
|
+
default:
|
|
50
|
+
throw new Error(`Unhandled type: ${action.type}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Define initial state
|
|
55
|
+
* @param data
|
|
56
|
+
*/
|
|
57
|
+
function stateInitializer(data) {
|
|
58
|
+
const __commentsObjectCacheKey = data.scLesson ? getLessonCommentsCacheKey(data.scLesson.id, data.next) : null;
|
|
59
|
+
let _initState = {
|
|
60
|
+
componentLoaded: false,
|
|
61
|
+
comments: [],
|
|
62
|
+
total: 0,
|
|
63
|
+
next: data.next,
|
|
64
|
+
previous: null,
|
|
65
|
+
isLoadingNext: false,
|
|
66
|
+
isLoadingPrevious: false,
|
|
67
|
+
page: Math.ceil(data.offset / data.pageSize + 1),
|
|
68
|
+
reload: false,
|
|
69
|
+
revalidate: false,
|
|
70
|
+
revalidateNext: null,
|
|
71
|
+
revalidatePrevious: null,
|
|
72
|
+
};
|
|
73
|
+
_initState['nextPage'] = _initState.next ? _initState.page + 1 : null;
|
|
74
|
+
_initState['previousPage'] = _initState.previous ? _initState.page - 1 : null;
|
|
75
|
+
if (__commentsObjectCacheKey && LRUCache.hasKey(__commentsObjectCacheKey) && data.cacheStrategy !== CacheStrategies.NETWORK_ONLY) {
|
|
76
|
+
const _cachedData = LRUCache.get(__commentsObjectCacheKey);
|
|
77
|
+
let page = Math.max(getCurrentPage(_cachedData.next, data.pageSize), 1);
|
|
78
|
+
const nextPage = _cachedData.next ? getCurrentPage(_cachedData.next, data.pageSize) : null;
|
|
79
|
+
const previousPage = _cachedData.previous ? Math.max(getCurrentPage(_cachedData.previous, data.pageSize) - 1, 1) : null;
|
|
80
|
+
return Object.assign(Object.assign({}, _initState), {
|
|
81
|
+
total: _cachedData.count,
|
|
82
|
+
next: _cachedData.next,
|
|
83
|
+
previous: _cachedData.previous,
|
|
84
|
+
comments: _cachedData.results,
|
|
85
|
+
page,
|
|
86
|
+
nextPage,
|
|
87
|
+
previousPage,
|
|
88
|
+
componentLoaded: true,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return _initState;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
:::info
|
|
95
|
+
This custom hooks is used to fetch paginated comments for a specific course lesson.
|
|
96
|
+
:::
|
|
97
|
+
* @param props
|
|
98
|
+
*/
|
|
99
|
+
export default function useSCFetchLessonCommentObjects(props) {
|
|
100
|
+
// PROPS
|
|
101
|
+
const { id, lessonObject, offset = 0, pageSize = 5, orderBy = SCCommentsOrderBy.ADDED_AT_DESC, parent, onChangePage, cacheStrategy = CacheStrategies.NETWORK_ONLY, } = props;
|
|
102
|
+
// FeedObject
|
|
103
|
+
const { scLesson, setSCLesson } = useSCFetchLesson({
|
|
104
|
+
id: id,
|
|
105
|
+
lesson: lessonObject,
|
|
106
|
+
courseId: lessonObject.course_id,
|
|
107
|
+
sectionId: lessonObject.section_id,
|
|
108
|
+
cacheStrategy,
|
|
109
|
+
});
|
|
110
|
+
/**
|
|
111
|
+
* Get next url
|
|
112
|
+
*/
|
|
113
|
+
const getNextUrl = () => {
|
|
114
|
+
const _offset = offset ? `&offset=${offset}` : '';
|
|
115
|
+
const _parent = parent ? `&parent=${parent}` : '';
|
|
116
|
+
const _lessonId = scLesson ? scLesson.id : id;
|
|
117
|
+
const _courseId = scLesson ? scLesson.course_id : lessonObject.course_id;
|
|
118
|
+
const _sectionId = scLesson ? scLesson.section_id : lessonObject.section_id;
|
|
119
|
+
return `${Endpoints.GetCourseLessonComments.url({
|
|
120
|
+
id: _courseId,
|
|
121
|
+
section_id: _sectionId,
|
|
122
|
+
lesson_id: _lessonId,
|
|
123
|
+
})}?limit=${pageSize}&ordering=${orderBy}${_offset}${_parent}`;
|
|
124
|
+
};
|
|
125
|
+
// STATE
|
|
126
|
+
const [state, dispatch] = useReducer(commentsReducer, { scLesson, offset, pageSize, next: getNextUrl(), cacheStrategy }, stateInitializer);
|
|
127
|
+
// REFS
|
|
128
|
+
const isMountedRef = useIsComponentMountedRef();
|
|
129
|
+
/**
|
|
130
|
+
* Get Comments (with cache)
|
|
131
|
+
*/
|
|
132
|
+
const revalidate = (url, forward) => {
|
|
133
|
+
return performFetchComments(url, false).then((res) => {
|
|
134
|
+
let _comments;
|
|
135
|
+
let page = getCurrentPage(forward ? res.next : res.previous, pageSize);
|
|
136
|
+
if (forward) {
|
|
137
|
+
let start = state.comments.slice(0, state.comments.length - res.results.length);
|
|
138
|
+
_comments = start.concat(res.results);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
let start = state.comments.slice(res.results.length, state.comments.length);
|
|
142
|
+
_comments = res.results.concat(start);
|
|
143
|
+
}
|
|
144
|
+
if (isMountedRef.current) {
|
|
145
|
+
dispatch({
|
|
146
|
+
type: forward ? commentsObjectActionTypes.DATA_NEXT_LOADED : commentsObjectActionTypes.DATA_PREVIOUS_LOADED,
|
|
147
|
+
payload: Object.assign(Object.assign({ page, comments: _comments }, (forward ? { next: res.next } : { previous: res.previous })), { total: res.count }),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
};
|
|
152
|
+
/**
|
|
153
|
+
* Get Comments
|
|
154
|
+
*/
|
|
155
|
+
const performFetchComments = (url, seekCache = true) => {
|
|
156
|
+
const __commentObjectsCacheKey = getLessonCommentsCacheKey(scLesson.id, url);
|
|
157
|
+
if (seekCache && LRUCache.hasKey(__commentObjectsCacheKey) && cacheStrategy !== CacheStrategies.NETWORK_ONLY) {
|
|
158
|
+
return Promise.resolve(LRUCache.get(__commentObjectsCacheKey));
|
|
159
|
+
}
|
|
160
|
+
return http
|
|
161
|
+
.request({
|
|
162
|
+
url,
|
|
163
|
+
method: Endpoints.GetCourseLessonComments.method,
|
|
164
|
+
})
|
|
165
|
+
.then((res) => {
|
|
166
|
+
if (res.status >= 300) {
|
|
167
|
+
return Promise.reject(res);
|
|
168
|
+
}
|
|
169
|
+
LRUCache.set(__commentObjectsCacheKey, res.data);
|
|
170
|
+
res.data.results.forEach((e) => LRUCache.set(getLessonCommentCacheKey(e.id), e));
|
|
171
|
+
return Promise.resolve(res.data);
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
/**
|
|
175
|
+
* Fetch previous comments
|
|
176
|
+
*/
|
|
177
|
+
function getPreviousPage() {
|
|
178
|
+
if (scLesson && state.previous && !state.isLoadingPrevious) {
|
|
179
|
+
const _previous = state.previous;
|
|
180
|
+
dispatch({ type: commentsObjectActionTypes.LOADING_PREVIOUS });
|
|
181
|
+
performFetchComments(_previous)
|
|
182
|
+
.then((res) => {
|
|
183
|
+
if (isMountedRef.current) {
|
|
184
|
+
let currentPage = getCurrentPage(_previous, pageSize);
|
|
185
|
+
let previousPage = res.previous ? currentPage - 1 : null;
|
|
186
|
+
dispatch({
|
|
187
|
+
type: commentsObjectActionTypes.DATA_PREVIOUS_LOADED,
|
|
188
|
+
payload: {
|
|
189
|
+
page: currentPage,
|
|
190
|
+
previousPage,
|
|
191
|
+
comments: [...res.results, ...state.comments],
|
|
192
|
+
previous: res.previous,
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
onChangePage && onChangePage(currentPage);
|
|
196
|
+
}
|
|
197
|
+
})
|
|
198
|
+
.catch((error) => {
|
|
199
|
+
Logger.error(SCOPE_SC_CORE, error);
|
|
200
|
+
})
|
|
201
|
+
.then(() => {
|
|
202
|
+
if (isMountedRef.current && cacheStrategy === CacheStrategies.STALE_WHILE_REVALIDATE) {
|
|
203
|
+
dispatch({ type: commentsObjectActionTypes.DATA_REVALIDATE, payload: { revalidatePrevious: _previous } });
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Fetch next comments
|
|
210
|
+
*/
|
|
211
|
+
function getNextPage() {
|
|
212
|
+
if (scLesson && state.next && !state.isLoadingNext) {
|
|
213
|
+
const _next = state.next;
|
|
214
|
+
dispatch({ type: commentsObjectActionTypes.LOADING_NEXT });
|
|
215
|
+
performFetchComments(_next)
|
|
216
|
+
.then((res) => {
|
|
217
|
+
if (isMountedRef.current) {
|
|
218
|
+
let currentPage = getCurrentPage(_next, pageSize);
|
|
219
|
+
let nextPage = res.next ? currentPage + 1 : null;
|
|
220
|
+
dispatch({
|
|
221
|
+
type: commentsObjectActionTypes.DATA_NEXT_LOADED,
|
|
222
|
+
payload: Object.assign({ page: currentPage, nextPage, comments: [...state.comments, ...res.results], next: res.next, total: res.count, componentLoaded: true }, (offset && state.comments.length === 0 ? { previous: res.previous, previousPage: res.previous ? currentPage - 1 : null } : {})),
|
|
223
|
+
});
|
|
224
|
+
onChangePage && onChangePage(currentPage);
|
|
225
|
+
}
|
|
226
|
+
})
|
|
227
|
+
.catch((error) => {
|
|
228
|
+
Logger.error(SCOPE_SC_CORE, error);
|
|
229
|
+
})
|
|
230
|
+
.then(() => {
|
|
231
|
+
if (isMountedRef.current && cacheStrategy === CacheStrategies.STALE_WHILE_REVALIDATE) {
|
|
232
|
+
dispatch({ type: commentsObjectActionTypes.DATA_REVALIDATE, payload: { revalidateNext: _next } });
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
/**
|
|
238
|
+
* Reset component status on change orderBy, pageSize, offset
|
|
239
|
+
*/
|
|
240
|
+
const reload = () => {
|
|
241
|
+
if (isMountedRef.current && state.componentLoaded && Boolean(scLesson) && !state.isLoadingNext && !state.reload) {
|
|
242
|
+
dispatch({
|
|
243
|
+
type: commentsObjectActionTypes.DATA_RELOAD,
|
|
244
|
+
payload: {
|
|
245
|
+
next: getNextUrl(),
|
|
246
|
+
},
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
/**
|
|
251
|
+
* Reload fetch comments
|
|
252
|
+
*/
|
|
253
|
+
useEffect(() => {
|
|
254
|
+
if (isMountedRef.current && state.componentLoaded && state.reload && !state.isLoadingNext && !state.isLoadingPrevious) {
|
|
255
|
+
dispatch({
|
|
256
|
+
type: commentsObjectActionTypes.DATA_RELOADED,
|
|
257
|
+
});
|
|
258
|
+
getNextPage();
|
|
259
|
+
}
|
|
260
|
+
}, [state.reload, isMountedRef]);
|
|
261
|
+
/**
|
|
262
|
+
* Revalidate last fetched comments
|
|
263
|
+
*/
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
if (isMountedRef.current && state.componentLoaded && Boolean(scLesson) && !state.reload && state.revalidate) {
|
|
266
|
+
revalidate(state.revalidateNext, Boolean(state.revalidateNext));
|
|
267
|
+
}
|
|
268
|
+
}, [state.revalidate, isMountedRef]);
|
|
269
|
+
/**
|
|
270
|
+
* Update lesson comments list
|
|
271
|
+
* @param updatedData
|
|
272
|
+
*/
|
|
273
|
+
const updateLessonComments = (updatedData) => {
|
|
274
|
+
if (updatedData) {
|
|
275
|
+
dispatch({
|
|
276
|
+
type: commentsObjectActionTypes.DATA_NEXT_LOADED,
|
|
277
|
+
payload: {
|
|
278
|
+
comments: updatedData,
|
|
279
|
+
next: state.next,
|
|
280
|
+
total: updatedData.length,
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
const __commentsCacheKey = getLessonCommentsCacheKey(scLesson.id, state.next);
|
|
284
|
+
LRUCache.set(__commentsCacheKey, updatedData.map((comment) => {
|
|
285
|
+
const __commentCacheKey = getLessonCommentCacheKey(comment.id);
|
|
286
|
+
LRUCache.set(__commentCacheKey, comment);
|
|
287
|
+
return comment.id;
|
|
288
|
+
}));
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
return Object.assign(Object.assign({ lessonObject: scLesson, setLessonObject: setSCLesson }, state), { pageSize,
|
|
292
|
+
getNextPage,
|
|
293
|
+
getPreviousPage,
|
|
294
|
+
orderBy,
|
|
295
|
+
reload,
|
|
296
|
+
updateLessonComments });
|
|
297
|
+
}
|
|
@@ -69,7 +69,7 @@ export default function useSCFetchUserBlockedBy({ user = null, blockedByUser = n
|
|
|
69
69
|
*/
|
|
70
70
|
useEffect(() => {
|
|
71
71
|
let interval;
|
|
72
|
-
if (scUserContext.user && blockedBy !== null && sync) {
|
|
72
|
+
if (scUserContext.user && scUserContext.user.id !== (user === null || user === void 0 ? void 0 : user.id) && blockedBy !== null && sync) {
|
|
73
73
|
interval = setInterval(() => {
|
|
74
74
|
fetchUserBlockedBy(user, false);
|
|
75
75
|
}, 5000);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { SCCourseType, SCUserType } from '@selfcommunity/types';
|
|
2
|
+
/**
|
|
3
|
+
:::info
|
|
4
|
+
This custom hook is used to manage the courses followed.
|
|
5
|
+
:::
|
|
6
|
+
|
|
7
|
+
:::tip How to use it:
|
|
8
|
+
Follow these steps:
|
|
9
|
+
```jsx
|
|
10
|
+
1. const scUserContext: SCUserContextType = useSCUser();
|
|
11
|
+
2. const scJoinedCoursesManager: SCJoinedCoursesManagerType = scUserContext.manager.courses;
|
|
12
|
+
3. scJoinedCoursesManager.isJoined(course)
|
|
13
|
+
```
|
|
14
|
+
:::
|
|
15
|
+
*/
|
|
16
|
+
export default function useSCJoinedCoursesManager(user?: SCUserType): {
|
|
17
|
+
courses: any[];
|
|
18
|
+
loading: any[];
|
|
19
|
+
isLoading: (v: number | {
|
|
20
|
+
id: number;
|
|
21
|
+
}) => boolean;
|
|
22
|
+
join?: undefined;
|
|
23
|
+
leave?: undefined;
|
|
24
|
+
joinStatus?: undefined;
|
|
25
|
+
refresh?: undefined;
|
|
26
|
+
emptyCache?: undefined;
|
|
27
|
+
} | {
|
|
28
|
+
courses: any[];
|
|
29
|
+
loading: any[];
|
|
30
|
+
isLoading: (v: number | {
|
|
31
|
+
id: number;
|
|
32
|
+
}) => boolean;
|
|
33
|
+
join: (course: SCCourseType, userId?: number) => Promise<any>;
|
|
34
|
+
leave: (course: SCCourseType) => Promise<any>;
|
|
35
|
+
joinStatus: (course: SCCourseType) => string;
|
|
36
|
+
refresh: () => void;
|
|
37
|
+
emptyCache: () => void;
|
|
38
|
+
};
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
2
|
+
import { Endpoints, http } from '@selfcommunity/api-services';
|
|
3
|
+
import { SCFeatureName, SCCoursePrivacyType, SCCourseJoinStatusType, SCNotificationTopicType, SCNotificationTypologyType, } from '@selfcommunity/types';
|
|
4
|
+
import useSCCachingManager from './useSCCachingManager';
|
|
5
|
+
import { SCOPE_SC_CORE } from '../constants/Errors';
|
|
6
|
+
import { Logger } from '@selfcommunity/utils';
|
|
7
|
+
import { useSCPreferences } from '../components/provider/SCPreferencesProvider';
|
|
8
|
+
import { SCNotificationMapping } from '../constants/Notification';
|
|
9
|
+
import { CONFIGURATIONS_COURSES_ENABLED } from '../constants/Preferences';
|
|
10
|
+
import { useDeepCompareEffectNoCheck } from 'use-deep-compare-effect';
|
|
11
|
+
import PubSub from 'pubsub-js';
|
|
12
|
+
/**
|
|
13
|
+
:::info
|
|
14
|
+
This custom hook is used to manage the courses followed.
|
|
15
|
+
:::
|
|
16
|
+
|
|
17
|
+
:::tip How to use it:
|
|
18
|
+
Follow these steps:
|
|
19
|
+
```jsx
|
|
20
|
+
1. const scUserContext: SCUserContextType = useSCUser();
|
|
21
|
+
2. const scJoinedCoursesManager: SCJoinedCoursesManagerType = scUserContext.manager.courses;
|
|
22
|
+
3. scJoinedCoursesManager.isJoined(course)
|
|
23
|
+
```
|
|
24
|
+
:::
|
|
25
|
+
*/
|
|
26
|
+
export default function useSCJoinedCoursesManager(user) {
|
|
27
|
+
const { cache, updateCache, emptyCache, data, setData, loading, setLoading, setUnLoading, isLoading } = useSCCachingManager();
|
|
28
|
+
const { preferences, features } = useSCPreferences();
|
|
29
|
+
const authUserId = user ? user.id : null;
|
|
30
|
+
const coursesEnabled = useMemo(() => preferences &&
|
|
31
|
+
features &&
|
|
32
|
+
features.includes(SCFeatureName.TAGGING) &&
|
|
33
|
+
features.includes(SCFeatureName.COURSE) &&
|
|
34
|
+
CONFIGURATIONS_COURSES_ENABLED in preferences &&
|
|
35
|
+
preferences[CONFIGURATIONS_COURSES_ENABLED].value, [preferences, features]);
|
|
36
|
+
const notificationInvitedToJoinCourse = useRef(null);
|
|
37
|
+
const notificationRequestedToJoinCourse = useRef(null);
|
|
38
|
+
const notificationAcceptedToJoinCourse = useRef(null);
|
|
39
|
+
const notificationAddedToCourse = useRef(null);
|
|
40
|
+
/**
|
|
41
|
+
* Subscribe to notification types user_follow, user_unfollow
|
|
42
|
+
*/
|
|
43
|
+
useDeepCompareEffectNoCheck(() => {
|
|
44
|
+
notificationInvitedToJoinCourse.current = PubSub.subscribe(`${SCNotificationTopicType.INTERACTION}.${SCNotificationTypologyType.USER_INVITED_TO_JOIN_COURSE}`, notificationSubscriber);
|
|
45
|
+
notificationRequestedToJoinCourse.current = PubSub.subscribe(`${SCNotificationTopicType.INTERACTION}.${SCNotificationTypologyType.USER_REQUESTED_TO_JOIN_COURSE}`, notificationSubscriber);
|
|
46
|
+
notificationAcceptedToJoinCourse.current = PubSub.subscribe(`${SCNotificationTopicType.INTERACTION}.${SCNotificationTypologyType.USER_ACCEPTED_TO_JOIN_COURSE}`, notificationSubscriber);
|
|
47
|
+
notificationAddedToCourse.current = PubSub.subscribe(`${SCNotificationTopicType.INTERACTION}.${SCNotificationTypologyType.USER_ADDED_TO_COURSE}`, notificationSubscriber);
|
|
48
|
+
return () => {
|
|
49
|
+
PubSub.unsubscribe(notificationInvitedToJoinCourse.current);
|
|
50
|
+
PubSub.unsubscribe(notificationRequestedToJoinCourse.current);
|
|
51
|
+
PubSub.unsubscribe(notificationAcceptedToJoinCourse.current);
|
|
52
|
+
PubSub.unsubscribe(notificationAddedToCourse.current);
|
|
53
|
+
};
|
|
54
|
+
}, [data]);
|
|
55
|
+
/**
|
|
56
|
+
* Notification subscriber handler
|
|
57
|
+
* @param msg
|
|
58
|
+
* @param dataMsg
|
|
59
|
+
*/
|
|
60
|
+
const notificationSubscriber = (msg, dataMsg) => {
|
|
61
|
+
if (dataMsg.data.course !== undefined) {
|
|
62
|
+
let _status;
|
|
63
|
+
switch (SCNotificationMapping[dataMsg.data.activity_type]) {
|
|
64
|
+
case SCNotificationTypologyType.USER_INVITED_TO_JOIN_COURSE:
|
|
65
|
+
_status = SCCourseJoinStatusType.INVITED;
|
|
66
|
+
break;
|
|
67
|
+
case SCNotificationTypologyType.USER_REQUESTED_TO_JOIN_COURSE:
|
|
68
|
+
_status = SCCourseJoinStatusType.REQUESTED;
|
|
69
|
+
break;
|
|
70
|
+
case SCNotificationTypologyType.USER_ACCEPTED_TO_JOIN_COURSE:
|
|
71
|
+
_status = SCCourseJoinStatusType.JOINED;
|
|
72
|
+
break;
|
|
73
|
+
case SCNotificationTypologyType.USER_ADDED_TO_COURSE:
|
|
74
|
+
_status = SCCourseJoinStatusType.JOINED;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
updateCache([dataMsg.data.course.id]);
|
|
78
|
+
setData((prev) => getDataUpdated(prev, dataMsg.data.course.id, _status));
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Memoized refresh all courses
|
|
83
|
+
* It makes a single request to the server and retrieves
|
|
84
|
+
* all the courses followed by the user in a single solution
|
|
85
|
+
* It might be useful for multi-tab sync
|
|
86
|
+
*/
|
|
87
|
+
const refresh = useMemo(() => () => {
|
|
88
|
+
emptyCache();
|
|
89
|
+
if (user) {
|
|
90
|
+
// Only if user is authenticated
|
|
91
|
+
http
|
|
92
|
+
.request({
|
|
93
|
+
url: Endpoints.GetJoinedCourses.url(),
|
|
94
|
+
method: Endpoints.GetJoinedCourses.method,
|
|
95
|
+
})
|
|
96
|
+
.then((res) => {
|
|
97
|
+
if (res.status >= 300) {
|
|
98
|
+
return Promise.reject(res);
|
|
99
|
+
}
|
|
100
|
+
const coursesIds = res.data.results.map((c) => c.id);
|
|
101
|
+
updateCache(coursesIds);
|
|
102
|
+
setData(res.data.results.map((c) => ({ [c.id]: c.join_status })));
|
|
103
|
+
return Promise.resolve(res.data);
|
|
104
|
+
})
|
|
105
|
+
.catch((e) => {
|
|
106
|
+
Logger.error(SCOPE_SC_CORE, 'Unable to refresh the authenticated user courses.');
|
|
107
|
+
Logger.error(SCOPE_SC_CORE, e);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}, [data, user, cache]);
|
|
111
|
+
/**
|
|
112
|
+
* Memoized join Course
|
|
113
|
+
* Toggle action
|
|
114
|
+
*/
|
|
115
|
+
const join = useMemo(() => (course, userId) => {
|
|
116
|
+
setLoading(course.id);
|
|
117
|
+
if (userId) {
|
|
118
|
+
return http
|
|
119
|
+
.request({
|
|
120
|
+
url: Endpoints.InviteOrAcceptUsersToCourse.url({ id: course.id }),
|
|
121
|
+
method: Endpoints.InviteOrAcceptUsersToCourse.method,
|
|
122
|
+
data: { users: [userId] },
|
|
123
|
+
})
|
|
124
|
+
.then((res) => {
|
|
125
|
+
if (res.status >= 300) {
|
|
126
|
+
return Promise.reject(res);
|
|
127
|
+
}
|
|
128
|
+
updateCache([course.id]);
|
|
129
|
+
setData((prev) => getDataUpdated(prev, course.id, SCCourseJoinStatusType.JOINED));
|
|
130
|
+
setUnLoading(course.id);
|
|
131
|
+
return Promise.resolve(res.data);
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
return http
|
|
136
|
+
.request({
|
|
137
|
+
url: Endpoints.JoinOrAcceptInviteToCourse.url({ id: course.id }),
|
|
138
|
+
method: Endpoints.JoinOrAcceptInviteToCourse.method,
|
|
139
|
+
})
|
|
140
|
+
.then((res) => {
|
|
141
|
+
if (res.status >= 300) {
|
|
142
|
+
return Promise.reject(res);
|
|
143
|
+
}
|
|
144
|
+
updateCache([course.id]);
|
|
145
|
+
setData((prev) => getDataUpdated(prev, course.id, course.privacy === SCCoursePrivacyType.PRIVATE && course.join_status !== SCCourseJoinStatusType.INVITED
|
|
146
|
+
? SCCourseJoinStatusType.REQUESTED
|
|
147
|
+
: SCCourseJoinStatusType.JOINED));
|
|
148
|
+
setUnLoading(course.id);
|
|
149
|
+
return Promise.resolve(res.data);
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}, [data, loading, cache]);
|
|
153
|
+
/**
|
|
154
|
+
* Memoized leave Course
|
|
155
|
+
* Toggle action
|
|
156
|
+
*/
|
|
157
|
+
const leave = useMemo(() => (course) => {
|
|
158
|
+
if (course.join_status !== SCCourseJoinStatusType.REQUESTED) {
|
|
159
|
+
setLoading(course.id);
|
|
160
|
+
return http
|
|
161
|
+
.request({
|
|
162
|
+
url: Endpoints.LeaveOrRemoveCourseRequest.url({ id: course.id }),
|
|
163
|
+
method: Endpoints.LeaveOrRemoveCourseRequest.method,
|
|
164
|
+
})
|
|
165
|
+
.then((res) => {
|
|
166
|
+
if (res.status >= 300) {
|
|
167
|
+
return Promise.reject(res);
|
|
168
|
+
}
|
|
169
|
+
updateCache([course.id]);
|
|
170
|
+
setData((prev) => getDataUpdated(prev, course.id, null));
|
|
171
|
+
setUnLoading(course.id);
|
|
172
|
+
return Promise.resolve(res.data);
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}, [data, loading, cache]);
|
|
176
|
+
/**
|
|
177
|
+
* Check the authenticated user subscription status to the course
|
|
178
|
+
* Update the courses cached
|
|
179
|
+
* Update courses subscription statuses
|
|
180
|
+
* @param course
|
|
181
|
+
*/
|
|
182
|
+
const checkCourseJoinedStatus = (course) => {
|
|
183
|
+
setLoading(course.id);
|
|
184
|
+
return http
|
|
185
|
+
.request({
|
|
186
|
+
url: Endpoints.GetCourseStatus.url({ id: course.id }),
|
|
187
|
+
method: Endpoints.GetCourseStatus.method,
|
|
188
|
+
})
|
|
189
|
+
.then((res) => {
|
|
190
|
+
if (res.status >= 300) {
|
|
191
|
+
return Promise.reject(res);
|
|
192
|
+
}
|
|
193
|
+
setData((prev) => getDataUpdated(prev, course.id, res.data.status));
|
|
194
|
+
updateCache([course.id]);
|
|
195
|
+
setUnLoading(course.id);
|
|
196
|
+
return Promise.resolve(res.data);
|
|
197
|
+
})
|
|
198
|
+
.catch((e) => {
|
|
199
|
+
setUnLoading(course.id);
|
|
200
|
+
return Promise.reject(e);
|
|
201
|
+
});
|
|
202
|
+
};
|
|
203
|
+
/**
|
|
204
|
+
* Get updated data
|
|
205
|
+
* @param data
|
|
206
|
+
* @param courseId
|
|
207
|
+
* @param joinStatus
|
|
208
|
+
*/
|
|
209
|
+
const getDataUpdated = (data, courseId, joinStatus) => {
|
|
210
|
+
const _index = data.findIndex((k) => parseInt(Object.keys(k)[0]) === courseId);
|
|
211
|
+
let _data;
|
|
212
|
+
if (_index < 0) {
|
|
213
|
+
_data = [...data, ...[{ [courseId]: joinStatus }]];
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
_data = data.map((k, i) => {
|
|
217
|
+
if (parseInt(Object.keys(k)[0]) === courseId) {
|
|
218
|
+
return { [Object.keys(k)[0]]: joinStatus };
|
|
219
|
+
}
|
|
220
|
+
return { [Object.keys(k)[0]]: data[i][Object.keys(k)[0]] };
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return _data;
|
|
224
|
+
};
|
|
225
|
+
/**
|
|
226
|
+
* Return current course subscription status if exists,
|
|
227
|
+
* otherwise return null
|
|
228
|
+
*/
|
|
229
|
+
const getCurrentCourseCacheStatus = useMemo(() => (course) => {
|
|
230
|
+
const d = data.filter((k) => parseInt(Object.keys(k)[0]) === course.id);
|
|
231
|
+
return d.length ? d[0][course.id] : !data.length ? course.join_status : null;
|
|
232
|
+
}, [data]);
|
|
233
|
+
/**
|
|
234
|
+
* Bypass remote check if the course is subscribed
|
|
235
|
+
*/
|
|
236
|
+
const getJoinStatus = useMemo(() => (course) => {
|
|
237
|
+
updateCache([course.id]);
|
|
238
|
+
setData((prev) => getDataUpdated(prev, course.id, course.join_status));
|
|
239
|
+
return course.join_status;
|
|
240
|
+
}, [data, cache]);
|
|
241
|
+
/**
|
|
242
|
+
* Memoized joinStatus
|
|
243
|
+
* If the course is already in cache -> check if the course is in courses,
|
|
244
|
+
* otherwise, check if user joined the course
|
|
245
|
+
*/
|
|
246
|
+
const joinStatus = useMemo(() => (course) => {
|
|
247
|
+
// Cache is valid also for anonymous user
|
|
248
|
+
if (cache.includes(course.id)) {
|
|
249
|
+
return getCurrentCourseCacheStatus(course);
|
|
250
|
+
}
|
|
251
|
+
if (authUserId) {
|
|
252
|
+
if ('join_status' in course) {
|
|
253
|
+
return getJoinStatus(course);
|
|
254
|
+
}
|
|
255
|
+
if (!isLoading(course)) {
|
|
256
|
+
checkCourseJoinedStatus(course);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}, [loading, cache, authUserId]);
|
|
261
|
+
/**
|
|
262
|
+
* Empty cache on logout
|
|
263
|
+
*/
|
|
264
|
+
useEffect(() => {
|
|
265
|
+
if (!authUserId) {
|
|
266
|
+
emptyCache();
|
|
267
|
+
}
|
|
268
|
+
}, [authUserId]);
|
|
269
|
+
if (!coursesEnabled || !user) {
|
|
270
|
+
return { courses: data, loading, isLoading };
|
|
271
|
+
}
|
|
272
|
+
return { courses: data, loading, isLoading, join, leave, joinStatus, refresh, emptyCache };
|
|
273
|
+
}
|
package/lib/esm/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Types
|
|
3
3
|
*/
|
|
4
|
-
import { SCUserContextType, SCFollowedCategoriesManagerType, SCContextProviderType, SCContextType, SCSettingsType, SCSessionType, SCFollowedManagerType, SCFollowersManagerType, SCSettingsManagerType, SCConnectionsManagerType, SCSubscribedIncubatorsManagerType, SCLocaleType, SCNotificationContextType, SCPreferencesContextType, SCThemeContextType, SCRoutingContextType, SCLocaleContextType, SCAlertMessagesContextType, SCThemeAvatarVariableType, SCThemeCategoryIconVariableType, SCThemeCategoryVariableType, SCThemeVariablesType, SCThemeType, SCSubscribedGroupsManagerType, SCSubscribedEventsManagerType } from './types';
|
|
4
|
+
import { SCUserContextType, SCFollowedCategoriesManagerType, SCContextProviderType, SCContextType, SCSettingsType, SCSessionType, SCFollowedManagerType, SCFollowersManagerType, SCSettingsManagerType, SCConnectionsManagerType, SCSubscribedIncubatorsManagerType, SCLocaleType, SCNotificationContextType, SCPreferencesContextType, SCThemeContextType, SCRoutingContextType, SCLocaleContextType, SCAlertMessagesContextType, SCThemeAvatarVariableType, SCThemeCategoryIconVariableType, SCThemeCategoryVariableType, SCThemeVariablesType, SCThemeType, SCSubscribedGroupsManagerType, SCSubscribedEventsManagerType, SCJoinedCoursesManagerType } from './types';
|
|
5
5
|
/**
|
|
6
6
|
* ContextProvider component
|
|
7
7
|
*/
|
|
@@ -47,6 +47,8 @@ import useSCFetchVote from './hooks/useSCFetchVote';
|
|
|
47
47
|
import useSCFetchFeedObject from './hooks/useSCFetchFeedObject';
|
|
48
48
|
import useSCFetchCommentObject from './hooks/useSCFetchCommentObject';
|
|
49
49
|
import useSCFetchCommentObjects from './hooks/useSCFetchCommentObjects';
|
|
50
|
+
import useSCFetchLessonCommentObject from './hooks/useSCFetchLessonCommentObject';
|
|
51
|
+
import useSCFetchLessonCommentObjects from './hooks/useSCFetchLessonCommentObjects';
|
|
50
52
|
import useSCFetchCustomAdv from './hooks/useSCFetchCustomAdv';
|
|
51
53
|
import useSCFetchTag from './hooks/useSCFetchTag';
|
|
52
54
|
import useSCFetchAddressingTagList from './hooks/useSCFetchAddressingTagList';
|
|
@@ -66,6 +68,9 @@ import useSCFetchEvent from './hooks/useSCFetchEvent';
|
|
|
66
68
|
import useSCFetchEvents from './hooks/useSCFetchEvents';
|
|
67
69
|
import useSCFetchLiveStream from './hooks/useSCFetchLiveStream';
|
|
68
70
|
import useSCGoogleApiLoader from './hooks/useSCGoogleApiLoader';
|
|
71
|
+
import useSCFetchCourse from './hooks/useSCFetchCourse';
|
|
72
|
+
import useSCFetchCourses from './hooks/useSCFetchCourses';
|
|
73
|
+
import useSCFetchLesson from './hooks/useSCFetchLesson';
|
|
69
74
|
/**
|
|
70
75
|
* Routing component
|
|
71
76
|
*/
|
|
@@ -88,4 +93,4 @@ import * as Preferences from './constants/Preferences';
|
|
|
88
93
|
/**
|
|
89
94
|
* List all exports
|
|
90
95
|
*/
|
|
91
|
-
export { SCUserContextType, SCFollowedCategoriesManagerType, SCContextProviderType, SCContextType, SCSettingsType, SCSessionType, SCSettingsManagerType, SCFollowedManagerType, SCFollowersManagerType, SCConnectionsManagerType, SCSubscribedIncubatorsManagerType, SCLocaleType, SCNotificationContextType, SCPreferencesContextType, SCThemeContextType, SCRoutingContextType, SCLocaleContextType, SCAlertMessagesContextType, SCThemeAvatarVariableType, SCThemeCategoryIconVariableType, SCThemeCategoryVariableType, SCThemeVariablesType, SCThemeType, SCSubscribedGroupsManagerType, SCSubscribedEventsManagerType, SCContext, SCUserContext, SCThemeContext, SCRoutingContext, SCLocaleContext, SCPreferencesContext, useSCContext, SCContextProvider, SCUserProvider, useSCUser, useSCPreferences, SCThemeProvider, useSCTheme, withSCTheme, getTheme, SCRoutingProvider, useSCRouting, SCLocaleProvider, useSCLocale, withSCLocale, SCPreferencesProvider, SCPreferences, SCFeatures, SCNotification, SCNotificationProvider, SCNotificationContext, useSCNotification, SCAlertMessagesProvider, SCAlertMessagesContext, useSCAlertMessages, Link, SCRoutes, SCCache, UserUtils, getEventStatus, Locale, Preferences, useSCFetchUser, useSCFetchUserProviders, useSCFetchVote, useSCFetchFeedObject, useSCFetchCommentObject, useSCFetchCommentObjects, useSCFetchCustomAdv, useSCFetchTag, useSCFetchAddressingTagList, useSCFetchCategory, useSCFetchCategories, useSCFetchIncubator, useSCMediaClick, useSCFetchContributors, useSCFetchFeed, useIsComponentMountedRef, usePreviousValue, useIsomorphicLayoutEffect, useEffectOnce, useNoInitialEffect, usePageVisibility, useSCFetchPrivateMessageSnippets, useSCFetchBroadcastMessages, useSCFetchUserBlockedBy, useSCUserIsBlocked, useSCFetchGroup, useSCFetchGroups, useSCFetchEvent, useSCFetchEvents, useSCFetchLiveStream, useSCGoogleApiLoader, };
|
|
96
|
+
export { SCUserContextType, SCFollowedCategoriesManagerType, SCContextProviderType, SCContextType, SCSettingsType, SCSessionType, SCSettingsManagerType, SCFollowedManagerType, SCFollowersManagerType, SCConnectionsManagerType, SCSubscribedIncubatorsManagerType, SCLocaleType, SCNotificationContextType, SCPreferencesContextType, SCThemeContextType, SCRoutingContextType, SCLocaleContextType, SCAlertMessagesContextType, SCThemeAvatarVariableType, SCThemeCategoryIconVariableType, SCThemeCategoryVariableType, SCThemeVariablesType, SCThemeType, SCSubscribedGroupsManagerType, SCSubscribedEventsManagerType, SCJoinedCoursesManagerType, SCContext, SCUserContext, SCThemeContext, SCRoutingContext, SCLocaleContext, SCPreferencesContext, useSCContext, SCContextProvider, SCUserProvider, useSCUser, useSCPreferences, SCThemeProvider, useSCTheme, withSCTheme, getTheme, SCRoutingProvider, useSCRouting, SCLocaleProvider, useSCLocale, withSCLocale, SCPreferencesProvider, SCPreferences, SCFeatures, SCNotification, SCNotificationProvider, SCNotificationContext, useSCNotification, SCAlertMessagesProvider, SCAlertMessagesContext, useSCAlertMessages, Link, SCRoutes, SCCache, UserUtils, getEventStatus, Locale, Preferences, useSCFetchUser, useSCFetchUserProviders, useSCFetchVote, useSCFetchFeedObject, useSCFetchCommentObject, useSCFetchCommentObjects, useSCFetchLessonCommentObject, useSCFetchLessonCommentObjects, useSCFetchCustomAdv, useSCFetchTag, useSCFetchAddressingTagList, useSCFetchCategory, useSCFetchCategories, useSCFetchIncubator, useSCMediaClick, useSCFetchContributors, useSCFetchFeed, useIsComponentMountedRef, usePreviousValue, useIsomorphicLayoutEffect, useEffectOnce, useNoInitialEffect, usePageVisibility, useSCFetchPrivateMessageSnippets, useSCFetchBroadcastMessages, useSCFetchUserBlockedBy, useSCUserIsBlocked, useSCFetchGroup, useSCFetchGroups, useSCFetchEvent, useSCFetchEvents, useSCFetchLiveStream, useSCGoogleApiLoader, useSCFetchCourse, useSCFetchCourses, useSCFetchLesson, };
|