Context.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. // NOTE: The purpose of this context is to immitate a Substrate storage for the forum until it's implemented as a substrate runtime module.
  2. import React, { useReducer, createContext, useContext } from 'react';
  3. import { Category, Thread, Reply } from '@joystream/types/forum';
  4. import { createType } from '@joystream/types';
  5. type CategoryId = number;
  6. type ThreadId = number;
  7. type ReplyId = number;
  8. export type ForumState = {
  9. sudo?: string;
  10. nextCategoryId: CategoryId;
  11. categoryById: Map<CategoryId, Category>;
  12. rootCategoryIds: CategoryId[];
  13. categoryIdsByParentId: Map<CategoryId, CategoryId[]>;
  14. nextThreadId: ThreadId;
  15. threadById: Map<ThreadId, Thread>;
  16. threadIdsByCategoryId: Map<CategoryId, ThreadId[]>;
  17. nextReplyId: ReplyId;
  18. replyById: Map<ReplyId, Reply>;
  19. replyIdsByThreadId: Map<ThreadId, ReplyId[]>;
  20. };
  21. const initialState: ForumState = {
  22. sudo: undefined,
  23. nextCategoryId: 1,
  24. categoryById: new Map<CategoryId, Category>(),
  25. rootCategoryIds: [],
  26. categoryIdsByParentId: new Map<CategoryId, CategoryId[]>(),
  27. nextThreadId: 1,
  28. threadById: new Map<ThreadId, Thread>(),
  29. threadIdsByCategoryId: new Map<CategoryId, ThreadId[]>(),
  30. nextReplyId: 1,
  31. replyById: new Map<ReplyId, Reply>(),
  32. replyIdsByThreadId: new Map<ThreadId, ReplyId[]>()
  33. };
  34. type SetForumSudo = {
  35. type: 'SetForumSudo';
  36. sudo?: string;
  37. };
  38. type NewCategoryAction = {
  39. type: 'NewCategory';
  40. category: Category;
  41. onCreated?: (newId: number) => void;
  42. };
  43. type UpdateCategoryAction = {
  44. type: 'UpdateCategory';
  45. category: Category;
  46. id: CategoryId;
  47. };
  48. type NewThreadAction = {
  49. type: 'NewThread';
  50. thread: Thread;
  51. onCreated?: (newId: number) => void;
  52. };
  53. type UpdateThreadAction = {
  54. type: 'UpdateThread';
  55. thread: Thread;
  56. id: ThreadId;
  57. };
  58. type ModerateThreadAction = {
  59. type: 'ModerateThread';
  60. id: ThreadId;
  61. moderator: string;
  62. rationale: string;
  63. };
  64. type NewReplyAction = {
  65. type: 'NewReply';
  66. reply: Reply;
  67. onCreated?: (newId: number) => void;
  68. };
  69. type UpdateReplyAction = {
  70. type: 'UpdateReply';
  71. reply: Reply;
  72. id: ReplyId;
  73. };
  74. type ModerateReplyAction = {
  75. type: 'ModerateReply';
  76. id: ReplyId;
  77. moderator: string;
  78. rationale: string;
  79. };
  80. type ForumAction =
  81. SetForumSudo |
  82. NewCategoryAction |
  83. UpdateCategoryAction |
  84. NewThreadAction |
  85. UpdateThreadAction |
  86. ModerateThreadAction |
  87. NewReplyAction |
  88. UpdateReplyAction |
  89. ModerateReplyAction;
  90. function reducer (state: ForumState, action: ForumAction): ForumState {
  91. switch (action.type) {
  92. case 'SetForumSudo': {
  93. const { sudo } = action;
  94. return {
  95. ...state,
  96. sudo
  97. };
  98. }
  99. case 'NewCategory': {
  100. const { category, onCreated } = action;
  101. const { parent_id } = category;
  102. let {
  103. nextCategoryId,
  104. categoryById,
  105. rootCategoryIds,
  106. categoryIdsByParentId
  107. } = state;
  108. if (parent_id) {
  109. let childrenIds = categoryIdsByParentId.get(parent_id.toNumber());
  110. if (!childrenIds) {
  111. childrenIds = [];
  112. }
  113. childrenIds.push(nextCategoryId);
  114. categoryIdsByParentId.set(parent_id.toNumber(), childrenIds);
  115. } else {
  116. if (!rootCategoryIds) {
  117. rootCategoryIds = [];
  118. }
  119. rootCategoryIds.push(nextCategoryId);
  120. }
  121. const newId = nextCategoryId;
  122. categoryById.set(newId, category);
  123. nextCategoryId = nextCategoryId + 1;
  124. if (onCreated) onCreated(newId);
  125. return {
  126. ...state,
  127. nextCategoryId,
  128. categoryById,
  129. rootCategoryIds,
  130. categoryIdsByParentId
  131. };
  132. }
  133. case 'UpdateCategory': {
  134. const { category, id } = action;
  135. const { categoryById } = state;
  136. categoryById.set(id, category);
  137. return {
  138. ...state,
  139. categoryById
  140. };
  141. }
  142. case 'NewThread': {
  143. const { thread, onCreated } = action;
  144. const { category_id } = thread;
  145. let {
  146. nextThreadId,
  147. threadById,
  148. threadIdsByCategoryId
  149. } = state;
  150. let threadIds = threadIdsByCategoryId.get(category_id.toNumber());
  151. if (!threadIds) {
  152. threadIds = [];
  153. threadIdsByCategoryId.set(category_id.toNumber(), threadIds);
  154. }
  155. threadIds.push(nextThreadId);
  156. const newId = nextThreadId;
  157. threadById.set(newId, thread);
  158. nextThreadId = nextThreadId + 1;
  159. if (onCreated) onCreated(newId);
  160. return {
  161. ...state,
  162. nextThreadId,
  163. threadById,
  164. threadIdsByCategoryId
  165. };
  166. }
  167. case 'UpdateThread': {
  168. const { thread, id } = action;
  169. const { threadById } = state;
  170. threadById.set(id, thread);
  171. return {
  172. ...state,
  173. threadById
  174. };
  175. }
  176. case 'ModerateThread': {
  177. const { id, moderator, rationale } = action;
  178. const { threadById } = state;
  179. const thread = threadById.get(id) as Thread;
  180. const moderation = createType('ModerationAction', {
  181. moderated_at: createType('BlockAndTime', {}),
  182. moderator_id: createType('AccountId', moderator),
  183. rationale: createType('Text', rationale)
  184. });
  185. const threadUpd = createType('Thread', {
  186. ...thread.cloneValues(),
  187. moderation: createType('Option<ModerationAction>', moderation)
  188. });
  189. threadById.set(id, threadUpd);
  190. return {
  191. ...state,
  192. threadById
  193. };
  194. }
  195. case 'NewReply': {
  196. const { reply, onCreated } = action;
  197. const { thread_id } = reply;
  198. let {
  199. nextReplyId,
  200. replyById,
  201. replyIdsByThreadId
  202. } = state;
  203. let replyIds = replyIdsByThreadId.get(thread_id.toNumber());
  204. if (!replyIds) {
  205. replyIds = [];
  206. replyIdsByThreadId.set(thread_id.toNumber(), replyIds);
  207. }
  208. replyIds.push(nextReplyId);
  209. const newId = nextReplyId;
  210. replyById.set(newId, reply);
  211. nextReplyId = nextReplyId + 1;
  212. if (onCreated) onCreated(newId);
  213. return {
  214. ...state,
  215. nextReplyId,
  216. replyById,
  217. replyIdsByThreadId
  218. };
  219. }
  220. case 'UpdateReply': {
  221. const { reply, id } = action;
  222. const { replyById } = state;
  223. replyById.set(id, reply);
  224. return {
  225. ...state,
  226. replyById
  227. };
  228. }
  229. case 'ModerateReply': {
  230. const { id, moderator, rationale } = action;
  231. const { replyById } = state;
  232. const reply = replyById.get(id) as Reply;
  233. const moderation = createType('ModerationAction', {
  234. moderated_at: createType('BlockAndTime', {}),
  235. moderator_id: createType('AccountId', moderator),
  236. rationale: createType('Text', rationale)
  237. });
  238. const replyUpd = createType('Reply', Object.assign(
  239. reply.cloneValues(),
  240. { moderation: createType('Option<ModerationAction>', moderation) }
  241. ));
  242. replyById.set(id, replyUpd);
  243. return {
  244. ...state,
  245. replyById
  246. };
  247. }
  248. default:
  249. throw new Error('Unexptected action: ' + JSON.stringify(action));
  250. }
  251. }
  252. function functionStub () {
  253. throw new Error('Function needs to be set in ForumProvider');
  254. }
  255. export type ForumContextProps = {
  256. state: ForumState;
  257. dispatch: React.Dispatch<ForumAction>;
  258. };
  259. const contextStub: ForumContextProps = {
  260. state: initialState,
  261. dispatch: functionStub
  262. };
  263. export const ForumContext = createContext<ForumContextProps>(contextStub);
  264. export function ForumProvider (props: React.PropsWithChildren<Record<any, unknown>>) {
  265. const [state, dispatch] = useReducer(reducer, initialState);
  266. return (
  267. <ForumContext.Provider value={{ state, dispatch }}>
  268. {props.children}
  269. </ForumContext.Provider>
  270. );
  271. }
  272. export function useForum () {
  273. return useContext(ForumContext);
  274. }