-
Notifications
You must be signed in to change notification settings - Fork 64
/
hooks.js
289 lines (250 loc) · 9.84 KB
/
hooks.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
import {
useCallback,
useContext, useEffect, useMemo, useRef, useState,
} from 'react';
import { breakpoints, useWindowSize } from '@openedx/paragon';
import { useDispatch, useSelector } from 'react-redux';
import {
matchPath, useLocation, useMatch, useNavigate,
} from 'react-router-dom';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { useIntl } from '@edx/frontend-platform/i18n';
import { AppContext } from '@edx/frontend-platform/react';
import selectCourseTabs from '../../components/NavigationBar/data/selectors';
import { LOADED } from '../../components/NavigationBar/data/slice';
import fetchTab from '../../components/NavigationBar/data/thunks';
import { ContentActions, RequestStatus, Routes } from '../../data/constants';
import { selectTopicsUnderCategory } from '../../data/selectors';
import fetchCourseBlocks from '../../data/thunks';
import DiscussionContext from '../common/context';
import PostCommentsContext from '../post-comments/postCommentsContext';
import { clearRedirect } from '../posts/data';
import { threadsLoadingStatus } from '../posts/data/selectors';
import { selectTopics } from '../topics/data/selectors';
import tourCheckpoints from '../tours/constants';
import selectTours from '../tours/data/selectors';
import { updateTourShowStatus } from '../tours/data/thunks';
import messages from '../tours/messages';
import { checkPermissions, discussionsPath } from '../utils';
import { ContentSelectors } from './constants';
import {
selectAreThreadsFiltered,
selectEnableInContext,
selectIsPostingEnabled,
selectIsUserLearner,
selectPostThreadCount,
selectUserHasModerationPrivileges,
selectUserIsGroupTa,
} from './selectors';
import fetchCourseConfig from './thunks';
export function useTotalTopicThreadCount() {
const topics = useSelector(selectTopics);
const count = useMemo(
() => (
Object.keys(topics)?.reduce((total, topicId) => {
const topic = topics[topicId];
return total + topic.threadCounts.discussion + topic.threadCounts.question;
}, 0)),
[],
);
return count;
}
export const useSidebarVisible = () => {
const location = useLocation();
const enableInContext = useSelector(selectEnableInContext);
const isViewingTopics = useMatch(Routes.TOPICS.ALL);
const isViewingLearners = useMatch(`${Routes.LEARNERS.PATH}/*`);
const isFiltered = useSelector(selectAreThreadsFiltered);
const totalThreads = useSelector(selectPostThreadCount);
const isThreadsEmpty = Boolean(useSelector(threadsLoadingStatus()) === RequestStatus.SUCCESSFUL && !totalThreads);
const matchInContextTopicView = Routes.TOPICS.PATH.find((route) => matchPath({ path: `${route}/*` }, location.pathname));
const isInContextTopicsView = Boolean(matchInContextTopicView && enableInContext);
const hideSidebar = Boolean(isThreadsEmpty && !isFiltered && !(isViewingTopics || isViewingLearners));
if (isInContextTopicsView) {
return true;
}
return !hideSidebar;
};
export function useCourseDiscussionData(courseId) {
const dispatch = useDispatch();
useEffect(() => {
async function fetchBaseData() {
await dispatch(fetchCourseConfig(courseId));
await dispatch(fetchTab(courseId));
}
fetchBaseData();
}, [courseId]);
}
export function useCourseBlockData(courseId) {
const dispatch = useDispatch();
const { authenticatedUser } = useContext(AppContext);
const { isEnrolled, courseStatus } = useSelector(selectCourseTabs);
const isUserLearner = useSelector(selectIsUserLearner);
useEffect(() => {
async function fetchBaseData() {
if (courseStatus === LOADED && (!isUserLearner || isEnrolled)) {
await dispatch(fetchCourseBlocks(courseId, authenticatedUser.username));
}
}
fetchBaseData();
}, [courseId, isEnrolled, courseStatus, isUserLearner]);
}
export function useRedirectToThread(courseId, enableInContextSidebar) {
const dispatch = useDispatch();
const navigate = useNavigate();
const location = useLocation();
const redirectToThread = useSelector(
(state) => state.threads.redirectToThread,
);
useEffect(() => {
// After posting a new thread we'd like to redirect users to it, the topic and post id are temporarily
// stored in redirectToThread
if (redirectToThread) {
dispatch(clearRedirect());
const newLocation = discussionsPath(Routes.COMMENTS.PAGES[enableInContextSidebar ? 'topics' : 'my-posts'], {
courseId,
postId: redirectToThread.threadId,
topicId: redirectToThread.topicId,
})(location);
navigate({ ...newLocation });
}
}, [redirectToThread]);
}
export function useIsOnDesktop() {
const windowSize = useWindowSize();
return windowSize.width >= breakpoints.medium.maxWidth;
}
export function useIsOnTablet() {
const windowSize = useWindowSize();
return windowSize.width >= breakpoints.small.maxWidth;
}
export function useIsOnXLDesktop() {
const windowSize = useWindowSize();
return windowSize.width >= breakpoints.extraLarge.maxWidth;
}
/**
* This hook posts a resize message to the parent window if running in an iframe
* @param refContainer reference to the component whose size is to be measured
*/
export function useContainerSize(refContainer) {
const location = useLocation();
const [height, setHeight] = useState();
const resizeObserver = useRef(new ResizeObserver(() => {
/* istanbul ignore if: ResizeObserver isn't available in the testing env */
window.requestAnimationFrame(() => {
if (refContainer?.current) {
setHeight(refContainer?.current?.clientHeight);
}
});
}));
useEffect(() => {
const container = refContainer?.current;
const observer = resizeObserver?.current;
if (container && observer) {
observer.observe(container);
setHeight(container.clientHeight);
}
return () => {
if (container && observer) {
observer.unobserve(container);
}
};
}, [refContainer, resizeObserver, location]);
return height;
}
export const useAlertBannerVisible = (
{
author, abuseFlagged, lastEdit, closed,
} = {},
) => {
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const userIsGroupTa = useSelector(selectUserIsGroupTa);
const userIsContentAuthor = getAuthenticatedUser().username === author;
const canSeeLastEditOrClosedAlert = (userHasModerationPrivileges || userIsContentAuthor || userIsGroupTa);
const canSeeReportedBanner = abuseFlagged;
return (
(canSeeLastEditOrClosedAlert && (lastEdit?.reason || closed)) || (canSeeReportedBanner)
);
};
/**
* React hook that gets the current topic ID from the current topic or category.
* The topicId in the DiscussionContext only return the direct topicId from the URL.
* If the URL has the current block ID it cannot get the topicID from that. This hook
* gets the topic ID from the URL if available, or from the current category otherwise.
* It only returns an ID if a single ID is available, if navigating a subsection it
* returns null.
* @returns {null|string} A topic ID if a single one available in the current context.
*/
export const useCurrentDiscussionTopic = () => {
const { topicId, category } = useContext(DiscussionContext);
const topics = useSelector(selectTopicsUnderCategory)(category);
if (topicId) {
return topicId;
}
if (topics?.length === 1) {
return topics[0];
}
return null;
};
export const useUserPostingEnabled = () => {
const isPostingEnabled = useSelector(selectIsPostingEnabled);
const userHasModerationPrivileges = useSelector(selectUserHasModerationPrivileges);
const isUserGroupTA = useSelector(selectUserIsGroupTa);
const isPrivileged = userHasModerationPrivileges || isUserGroupTA;
return (isPostingEnabled || isPrivileged);
};
function camelToConstant(string) {
return string.replace(/[A-Z]/g, (match) => `_${match}`).toUpperCase();
}
export const useTourConfiguration = () => {
const intl = useIntl();
const dispatch = useDispatch();
const { enableInContextSidebar } = useContext(DiscussionContext);
const tours = useSelector(selectTours);
const handleOnDismiss = useCallback((id) => (
dispatch(updateTourShowStatus(id))
), []);
const handleOnEnd = useCallback((id) => (
dispatch(updateTourShowStatus(id))
), []);
const toursConfig = useMemo(() => (
tours?.map((tour) => Object.keys(tourCheckpoints(intl)).includes(tour.tourName) && (
{
tourId: tour.tourName,
advanceButtonText: intl.formatMessage(messages.advanceButtonText),
dismissButtonText: intl.formatMessage(messages.dismissButtonText),
endButtonText: intl.formatMessage(messages.endButtonText),
enabled: tour && Boolean(tour.enabled && tour.showTour && !enableInContextSidebar),
onDismiss: () => handleOnDismiss(tour.id),
onEnd: () => handleOnEnd(tour.id),
checkpoints: tourCheckpoints(intl)[camelToConstant(tour.tourName)],
}
))
), [tours, enableInContextSidebar]);
return toursConfig;
};
export const useDebounce = (value, delay) => {
// State and setters for debounced value
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler);
};
},
[value, delay], // Only re-call effect if value or delay changes
);
return debouncedValue;
};
export const useHasLikePermission = (contentType, id) => {
const { postType } = useContext(PostCommentsContext);
const content = { ...useSelector(ContentSelectors[contentType](id)), postType };
return checkPermissions(content, ContentActions.VOTE);
};