tipsypastels

Untitled

Sep 18th, 2020 (edited)
312
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. import { Dispatch, useReducer, useEffect, lazy, useRef, RefObject } from 'react';
  2. import { SelectedPostList } from 'sites/pc3/TopicPage/useSelectedPosts';
  3. import { YearDay } from 'artboard/DateTimePicker';
  4. import { useLazyQuery } from '@apollo/react-hooks';
  5. import { ComposerQuery, ComposerQuery_me } from '__generated__';
  6. import gql from 'graphql-tag';
  7.  
  8. const COMPOSER_QUERY = gql`
  9.   query ComposerQuery {
  10.     me {
  11.       id
  12.       username
  13.       avatar {
  14.         url
  15.       }
  16.       staffRoles: roles(allowStaffPosting: true) {
  17.         id
  18.         title
  19.         icon
  20.         color
  21.       }
  22.     }
  23.   }
  24. `;
  25.  
  26. export const COMPOSER_POPUPS = {
  27.   'font.family': lazy(() => import('./modules/popups/FontFamilyPopup')),
  28.   'font.size': lazy(() => import('./modules/popups/FontSizePopup')),
  29. };
  30.  
  31. export type ComposerPopupName = keyof typeof COMPOSER_POPUPS;
  32.  
  33. export enum ComposerFlags {
  34.   SUBMIT      = 1 << 0,
  35.   ATTACH      = 1 << 1,
  36.   STAFF_POST  = 1 << 2,
  37.   DRAFT       = 1 << 3,
  38.   SCHEDULE    = 1 << 4,
  39.   QUICK_CLOSE  = 1 << 5,
  40.   QUICK_STICK = 1 << 6,
  41. }
  42.  
  43. /** An object that can be edited as a post. */
  44. export type ComposerEditable = {
  45.   id: string;
  46.   type: string;
  47.   content: string;
  48. }
  49.  
  50. /** Options passed to the `useComposer` hook. */
  51. export type ComposerOpts = {
  52.   /** The base name used for non-edit cache keys, such as "topic". */
  53.   domainId: string;
  54.   /** A list of features that are enabled for this composer. */
  55.   flags: number;
  56.   /** Callback to be called with the composer state when submitting. */
  57.   onSubmit: unknown;
  58.   /** A list of selected post ids. */
  59.   selectedPosts: SelectedPostList;
  60.   /** Terms used for dynamic copy about the container of the new post. */
  61.   term?: { asTitle: string, asWord: string };
  62. }
  63.  
  64. /** Composer state as formed by the reducer. */
  65. type ComposerInternalState = {
  66.   /** Whether the composer is open. */
  67.   isOpen: boolean;
  68.   /** The post currently being edited. */
  69.   editing: ComposerEditable | null;
  70.   /** The content of the post being written. */
  71.   content: string;
  72.   /** Whether a cached value was loaded before being edited. */
  73.   isFromCache: boolean;
  74.   /** The day this post is scheduled to publish, if scheduled. */
  75.   scheduleDay: YearDay | null;
  76.   /** The visibility this post is scheduled to publish with. */
  77.   visibility: 'published' | 'draft' | 'scheduled';
  78.   /** The staff role this post will publish as. */
  79.   staffRole: string | null;
  80.   /** The current popup menu in the Write tab. */
  81.   currentPopup: ComposerPopupName | null;
  82.   /** Whether the topic should be closed on submitting. */
  83.   closeOnSubmit: boolean;
  84.   /** Whether the topic should be stickied on submitting. */
  85.   stickyOnSubmit: boolean;
  86.   /** Whether the composer should automatically cache changes periodically. */
  87.   doAutoCache: boolean;
  88. }
  89.  
  90. type ComposerInternalAction =
  91.   | { type: 'OPEN_MENU' }
  92.   | { type: 'CLOSE_MENU' }
  93.   | { type: 'CREATE_POST' }
  94.   | { type: 'EDIT_POST', post: ComposerEditable }
  95.   | { type: 'SET_CONTENT', content: string }
  96.   | { type: 'SET_SCHEDULE_DAY', scheduleDay: YearDay | null }
  97.   | { type: 'SET_VIS', vis: ComposerState['visibility'] }
  98.   | { type: 'SET_STAFF_ROLE', staffRole: string | null }
  99.   | { type: 'OPEN_POPUP', popup: ComposerPopupName }
  100.   | { type: 'CLOSE_POPUP' }
  101.   | { type: 'UNCACHE_AND_RELOAD' }
  102.   | { type: 'SET_STICKY_ON_SUBMIT', stickyOnSubmit: boolean }
  103.   | { type: 'SET_CLOSE_ON_SUBMIT', closeOnSubmit: boolean }
  104.   | { type: 'SET_AUTO_CACHE', doAutoCache: boolean }
  105.  
  106. /**
  107.  * The state of a composer hook.
  108.  * Stores both stateful and computed values about everything going on
  109.  * inside the composer, along with a `dispatch` method for applying changes.
  110.  */
  111. export type ComposerState =
  112.   & ComposerInternalState
  113.   & { dispatch: Dispatch<ComposerInternalAction> }
  114.   & {
  115.     /** The current user as seen by the composer. */
  116.     me: ComposerQuery_me | null | undefined;
  117.     /** Whether a given flag is enabled for the composer. */
  118.     hasFlag(flag: number): boolean;
  119.     /** Whether the composer can be submitted. */
  120.     canSubmit: boolean;
  121.     /** Copy for the topic type or related. */
  122.     term: undefined | ComposerOpts['term'];
  123.     /** A reference to the textarea itself. */
  124.     textareaRef: RefObject<HTMLTextAreaElement>;
  125.   }
  126.  
  127. function useComposer(opts: ComposerOpts): ComposerState {
  128.   const { domainId, flags, selectedPosts, term } = opts;
  129.   const textareaRef = useRef<HTMLTextAreaElement>(null);
  130.  
  131.   function reducer(
  132.     state: ComposerInternalState,
  133.     action: ComposerInternalAction,
  134.   ): ComposerInternalState {
  135.     switch (action.type) {
  136.       case 'OPEN_MENU': {
  137.         return { ...state, isOpen: true }
  138.       }
  139.       case 'CLOSE_MENU': {
  140.         return { ...state, isOpen: false }
  141.       }
  142.       case 'SET_CONTENT': {
  143.         return { ...state, content: action.content, isFromCache: false };
  144.       }
  145.       case 'EDIT_POST': {
  146.         return {
  147.           ...state,
  148.           ...deriveInitialContentForEdit(action.post),
  149.           isOpen: true,
  150.         }
  151.       }
  152.       case 'CREATE_POST': {
  153.         return {
  154.           ...state,
  155.           ...deriveInitialContentForNew(domainId, selectedPosts),
  156.           isOpen: true,
  157.         }
  158.       }
  159.       case 'UNCACHE_AND_RELOAD': {
  160.         if (state.editing) {
  161.           return {
  162.             ...state,
  163.             isFromCache: false,
  164.             content: state.editing.content,
  165.           };
  166.         } else {
  167.           return {
  168.             ...state,
  169.             isFromCache: false,
  170.             content: baseContentForNew(selectedPosts),
  171.           };
  172.         }
  173.       }
  174.       case 'OPEN_POPUP': {
  175.         return { ...state, currentPopup: action.popup };
  176.       }
  177.       case 'CLOSE_POPUP': {
  178.         return { ...state, currentPopup: null };
  179.       }
  180.       case 'SET_STAFF_ROLE': {
  181.         return { ...state, staffRole: action.staffRole };
  182.       }
  183.       case 'SET_SCHEDULE_DAY': {
  184.         return { ...state, scheduleDay: action.scheduleDay };
  185.       }
  186.       case 'SET_VIS': {
  187.         return { ...state, visibility: action.vis };
  188.       }
  189.       case 'SET_CLOSE_ON_SUBMIT': {
  190.         return { ...state, closeOnSubmit: action.closeOnSubmit };
  191.       }
  192.       case 'SET_STICKY_ON_SUBMIT': {
  193.         return { ...state, stickyOnSubmit: action.stickyOnSubmit };
  194.       }
  195.       case 'SET_AUTO_CACHE': {
  196.         return { ...state, doAutoCache: action.doAutoCache };
  197.       }
  198.     }
  199.   }
  200.  
  201.   const [state, dispatch] = useReducer(reducer, {
  202.     ...deriveInitialContentForNew(domainId),
  203.     isOpen: __DEV__,
  204.     editing: null,
  205.     scheduleDay: null,
  206.     staffRole: null,
  207.     currentPopup: null,
  208.     visibility: 'published',
  209.     stickyOnSubmit: false,
  210.     closeOnSubmit: false,
  211.     doAutoCache: false,
  212.   });
  213.  
  214.   const [getMe, { data }] = useLazyQuery<ComposerQuery>(COMPOSER_QUERY);
  215.   const me = data?.me;
  216.  
  217.   useEffect(() => {
  218.       if (!state.isOpen) {
  219.         saveToCache(state.content, state.editing ?? domainId)
  220.       }
  221.     },
  222.     [state.isOpen, state.content, state.editing, domainId],
  223.   );
  224.  
  225.   useEffect(
  226.     () => { state.isOpen && getMe() },
  227.     [state.isOpen, getMe],
  228.   );
  229.  
  230.   function hasFlag(flag: number) {
  231.     return (flags & flag) === flag;
  232.   }
  233.  
  234.   let canSubmit = state.content.length > 0;
  235.  
  236.   if (state.visibility === 'scheduled') {
  237.     canSubmit = canSubmit && state.scheduleDay != null;
  238.   }
  239.  
  240.   return {
  241.     ...state,
  242.     dispatch,
  243.     me,
  244.     term,
  245.     hasFlag,
  246.     canSubmit,
  247.     textareaRef,
  248.   };
  249. }
  250.  
  251. export default useComposer;
  252.  
  253. function deriveInitialContentForNew(
  254.   domainId: string,
  255.   selectedPosts?: SelectedPostList,
  256. ) {
  257.   const cachedContent = getCachedContent(domainId);
  258.   if (cachedContent) {
  259.     return { content: cachedContent, isFromCache: true };
  260.   }
  261.  
  262.   return {
  263.     content: baseContentForNew(selectedPosts),
  264.     isFromCache: false,
  265.   };
  266. }
  267.  
  268. function deriveInitialContentForEdit(post: ComposerEditable) {
  269.   const cachedEdit = getCachedContent(post);
  270.   if (cachedEdit) {
  271.     return { content: cachedEdit, isFromCache: true };
  272.   }
  273.  
  274.   return { content: post.content, isFromCache: false };
  275. }
  276.  
  277. function baseContentForNew(selectedPosts?: SelectedPostList) {
  278.   return '';
  279. }
  280.  
  281. function getCachedContent(domainIdOrEditable: string | ComposerEditable) {
  282.   return localStorage.getItem(getCacheKey(domainIdOrEditable));
  283. }
  284.  
  285. function saveToCache(content: string, domainIdOrEditable: string | ComposerEditable) {
  286.   if (!content) {
  287.     return;
  288.   }
  289.  
  290.   const cacheKey = getCacheKey(domainIdOrEditable);
  291.  
  292.   if (__DEV__) {
  293.     console.log(`Composer: saved ${content} to ${cacheKey}`);
  294.   }
  295.  
  296.   localStorage.setItem(cacheKey, content);
  297. }
  298.  
  299. function getCacheKey(domainIdOrEditable: string | ComposerEditable) {
  300.   if (typeof domainIdOrEditable === 'string') {
  301.     return `composer-cache-new-${domainIdOrEditable}`;
  302.   }
  303.  
  304.   return `composer-cache-editing-${domainIdOrEditable.type}:${domainIdOrEditable.id}`;
  305. }
  306.  
  307. function deserializeState() {
  308.  
  309. }
Add Comment
Please, Sign In to add comment