/library/src/scripts/features/discussions/discussionHooks.tsx

https://github.com/vanilla/vanilla · TypeScript · 233 lines · 190 code · 39 blank · 4 comment · 24 complexity · f2ba7268f8547069d7087f6eb9dc09d5 MD5 · raw file

  1. /**
  2. * @copyright 2009-2020 Vanilla Forums Inc.
  3. * @license GPL-2.0-only
  4. */
  5. import DiscussionActions, {
  6. IAnnounceDiscussionParams,
  7. IDeleteDiscussionReaction,
  8. IGetDiscussionByID,
  9. IMoveDiscussionParams,
  10. IPostDiscussionReaction,
  11. IPutDiscussionBookmarked,
  12. useDiscussionActions,
  13. } from "@library/features/discussions/DiscussionActions";
  14. import { IDiscussionsStoreState } from "@library/features/discussions/discussionsReducer";
  15. import { useDispatch, useSelector } from "react-redux";
  16. import { ILoadable, LoadStatus } from "@library/@types/api/core";
  17. import { useCallback, useEffect, useState } from "react";
  18. import { IDiscussion, IGetDiscussionListParams } from "@dashboard/@types/api/discussion";
  19. import { stableObjectHash } from "@vanilla/utils";
  20. import { useCurrentUserID } from "@library/features/users/userHooks";
  21. import { hasPermission, PermissionMode } from "@library/features/users/Permission";
  22. import { usePermissions } from "@library/features/users/userModel";
  23. import { getMeta } from "@library/utility/appUtils";
  24. import { useUniqueID } from "@library/utility/idUtils";
  25. export function useDiscussion(discussionID: IGetDiscussionByID["discussionID"]): ILoadable<IDiscussion> {
  26. const actions = useDiscussionActions();
  27. const existingResult = useSelector((state: IDiscussionsStoreState) => {
  28. return {
  29. status: state.discussions.discussionsByID[discussionID]
  30. ? LoadStatus.SUCCESS
  31. : state.discussions.fullRecordStatusesByID[discussionID]?.status ?? LoadStatus.PENDING,
  32. data: state.discussions.discussionsByID[discussionID],
  33. };
  34. });
  35. const { status } = existingResult;
  36. useEffect(() => {
  37. if (LoadStatus.PENDING.includes(status)) {
  38. actions.getDiscussionByID({ discussionID });
  39. }
  40. }, [status, actions, discussionID]);
  41. return existingResult;
  42. }
  43. export function useToggleDiscussionBookmarked(discussionID: IPutDiscussionBookmarked["discussionID"]) {
  44. const { putDiscussionBookmarked } = useDiscussionActions();
  45. async function toggleDiscussionBookmarked(bookmarked: IPutDiscussionBookmarked["bookmarked"]) {
  46. return await putDiscussionBookmarked({
  47. discussionID,
  48. bookmarked,
  49. });
  50. }
  51. return toggleDiscussionBookmarked;
  52. }
  53. export function useCurrentDiscussionReaction(discussionID: IDiscussion["discussionID"]) {
  54. return useSelector(function (state: IDiscussionsStoreState) {
  55. return state.discussions.discussionsByID[discussionID]?.reactions?.find(({ hasReacted }) => hasReacted);
  56. });
  57. }
  58. export function useReactToDiscussion(discussionID: IPostDiscussionReaction["discussionID"]) {
  59. const { postDiscussionReaction } = useDiscussionActions();
  60. const currentReaction = useCurrentDiscussionReaction(discussionID);
  61. async function reactToDiscussion(reaction: IPostDiscussionReaction["reaction"]) {
  62. return await postDiscussionReaction({
  63. discussionID,
  64. reaction,
  65. currentReaction,
  66. });
  67. }
  68. return reactToDiscussion;
  69. }
  70. export function useRemoveDiscussionReaction(discussionID: IDeleteDiscussionReaction["discussionID"]) {
  71. const { deleteDiscussionReaction } = useDiscussionActions();
  72. const currentReaction = useCurrentDiscussionReaction(discussionID)!;
  73. async function removeDiscussionReaction() {
  74. return await deleteDiscussionReaction({
  75. discussionID,
  76. currentReaction,
  77. });
  78. }
  79. return removeDiscussionReaction;
  80. }
  81. export function useDiscussionList(
  82. apiParams: IGetDiscussionListParams,
  83. prehydratedItems?: IDiscussion[],
  84. ): ILoadable<IDiscussion[]> {
  85. const dispatch = useDispatch();
  86. const actions = useDiscussionActions();
  87. const paramHash = stableObjectHash(apiParams);
  88. useEffect(() => {
  89. if (prehydratedItems) {
  90. dispatch(
  91. DiscussionActions.getDiscussionListACs.done({
  92. params: apiParams,
  93. result: prehydratedItems,
  94. }),
  95. );
  96. } else {
  97. actions.getDiscussionList(apiParams);
  98. }
  99. }, [prehydratedItems, apiParams, paramHash, dispatch, actions]);
  100. const loadStatus = useSelector(
  101. (state: IDiscussionsStoreState) =>
  102. state.discussions.discussionIDsByParamHash[paramHash]?.status ?? LoadStatus.PENDING,
  103. );
  104. const discussions = useSelector((state: IDiscussionsStoreState) => {
  105. return loadStatus === LoadStatus.SUCCESS
  106. ? state.discussions.discussionIDsByParamHash[paramHash].data!.map(
  107. (discussionID) => state.discussions.discussionsByID[discussionID],
  108. )
  109. : [];
  110. });
  111. return {
  112. status: loadStatus,
  113. data: discussions,
  114. };
  115. }
  116. export function useUserCanEditDiscussion(discussion: IDiscussion) {
  117. usePermissions();
  118. const currentUserID = useCurrentUserID();
  119. const currentUserIsDiscussionAuthor = discussion.insertUserID === currentUserID;
  120. const now = new Date();
  121. const cutoff =
  122. getMeta("ui.editContentTimeout", -1) > -1
  123. ? new Date(new Date(discussion.dateInserted).getTime() + getMeta("ui.editContentTimeout") * 1000)
  124. : null;
  125. return (
  126. hasPermission("discussions.manage", {
  127. mode: PermissionMode.RESOURCE_IF_JUNCTION,
  128. resourceType: "category",
  129. resourceID: discussion.categoryID,
  130. }) ||
  131. (currentUserIsDiscussionAuthor && !discussion.closed && (cutoff === null || now < cutoff))
  132. );
  133. }
  134. function usePatchStatus(discussionID: number, patchID: string): LoadStatus {
  135. return useSelector((state: IDiscussionsStoreState) => {
  136. return state.discussions.patchStatusByPatchID[`${discussionID}-${patchID}`]?.status ?? LoadStatus.PENDING;
  137. });
  138. }
  139. export function useDiscussionPatch(discussionID: number, patchID: string | null = null) {
  140. const ownID = useUniqueID("discussionPatch");
  141. const actualPatchID = patchID ?? ownID;
  142. const isLoading = usePatchStatus(discussionID, actualPatchID) === LoadStatus.LOADING;
  143. const actions = useDiscussionActions();
  144. const patchDiscussion = useCallback(
  145. (query: Omit<Parameters<typeof actions.patchDiscussion>[0], "discussionID" | "patchStatusID">) => {
  146. return actions.patchDiscussion({
  147. discussionID,
  148. patchStatusID: actualPatchID,
  149. ...query,
  150. });
  151. },
  152. [actualPatchID, actions, discussionID],
  153. );
  154. return {
  155. isLoading,
  156. patchDiscussion: patchDiscussion,
  157. };
  158. }
  159. function useDiscussionPutTypeStatus(discussionID: number): LoadStatus {
  160. return useSelector((state: IDiscussionsStoreState) => {
  161. return state.discussions.changeTypeByID[discussionID]?.status ?? LoadStatus.PENDING;
  162. });
  163. }
  164. export function useDiscussionPutType(discussionID: number) {
  165. const isLoading = useDiscussionPutTypeStatus(discussionID) === LoadStatus.LOADING;
  166. const actions = useDiscussionActions();
  167. const putDiscussionType = useCallback(
  168. (query: Omit<Parameters<typeof actions.putDiscussionType>[0], "discussionID">) => {
  169. return actions.putDiscussionType({
  170. discussionID,
  171. ...query,
  172. });
  173. },
  174. [actions, discussionID],
  175. );
  176. return {
  177. isLoading,
  178. putDiscussionType: putDiscussionType,
  179. };
  180. }
  181. export function usePutDiscussionTags(discussionID: number) {
  182. const actions = useDiscussionActions();
  183. async function putDiscussionTags(tagIDs: number[]) {
  184. try {
  185. await actions.putDiscussionTags({
  186. discussionID,
  187. tagIDs,
  188. });
  189. } catch (error) {
  190. throw new Error(error.description); //fixme: what we really want is an object that we can pass wholesale to formik's setError() function
  191. }
  192. }
  193. return putDiscussionTags;
  194. }