cyb/src/features/studio/studio.context.tsx

import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { createSearchParams, useSearchParams } from 'react-router-dom';
import { CID_TWEET } from 'src/constants/app';
import { useBackend } from 'src/contexts/backend/backend';
import useDebounce from 'src/hooks/useDebounce';
import useParticle from 'src/hooks/useParticle';
import { addIfpsMessageOrCid } from 'src/utils/ipfs/helpers';
import useAdviserTexts from '../adviser/useAdviserTexts';

type StateActionBar = 'link' | 'keywords-from' | 'keywords-to';

export type KeywordsItem = {
  text: string;
  cid: string;
};

const defaultKeywordsFrom: KeywordsItem = {
  text: 'tweet',
  cid: CID_TWEET,
};

const StudioContext = React.createContext<{
  stateActionBar: StateActionBar;
  keywordsFrom: KeywordsItem[];
  keywordsTo: KeywordsItem[];
  currentMarkdown: string;
  loadedMarkdown: string;
  lastCid: string | undefined;
  setStateActionBar: React.Dispatch<React.SetStateAction<StateActionBar>>;
  addKeywords: (type: 'from' | 'to', item: KeywordsItem[]) => void;
  removeKeywords: (type: 'from' | 'to', item: string) => void;
  saveMarkdown: (md: string) => void;
}>({
  stateActionBar: 'link',
  currentMarkdown: '',
  loadedMarkdown: '',
  lastCid: '',
  keywordsFrom: [],
  keywordsTo: [],
  setStateActionBar: () => {},
  addKeywords: () => {},
  removeKeywords: () => {},
  saveMarkdown: () => {},
});

export const useStudioContext = () => React.useContext(StudioContext);

function StudioContextProvider({ children }: { children: React.ReactNode }) {
  const { isIpfsInitialized, ipfsApi } = useBackend();
  const [cidSearchParams, setCidSearchParams] = useState<string | undefined>();
  const [lastCid, setLastCid] = useState<string | undefined>();

  const firstEffectOccured = useRef(false);
  const [searchParams, setSearchParams] = useSearchParams();
  const updateSearchParams = useCallback(setSearchParams, [setSearchParams]);

  const { status, details } = useParticle(cidSearchParams!);
  const content = details && details.type === 'text' && details.content ? details.content : '';

  const [stateActionBar, setStateActionBar] = useState<StateActionBar>('link');
  const [keywordsFrom, setKeywordsFrom] = useState<KeywordsItem[]>([defaultKeywordsFrom]);
  const [keywordsTo, setKeywordsTo] = useState<KeywordsItem[]>([]);
  const [currentMarkdown, setCurrentMarkdown] = useState<string>('');

  const { debounce } = useDebounce();

  const { setAdviser } = useAdviserTexts({
    isLoading: (!cidSearchParams && status === 'pending') || !isIpfsInitialized,
    loadingText: !isIpfsInitialized ? 'node is loading' : 'cid is loading',
    defaultText: 'you can create content',
    error:
      details && details.type !== 'text'
        ? 'invalid content type, content must be text type'
        : undefined,
  });

  useEffect(() => {
    if (firstEffectOccured.current) {
      return;
    }

    firstEffectOccured.current = true;
    const param = Object.fromEntries(searchParams.entries());

    if (Object.keys(param).length > 0) {
      const { cid, particle } = param;

      if (cid) {
        setCidSearchParams(cid);
        setLastCid(cid);
      } else if (particle && ipfsApi) {
        // particle is text โ€” save to IPFS and load into editor
        addIfpsMessageOrCid(particle, { ipfsApi }).then((particleCid) => {
          setCidSearchParams(particleCid);
          setLastCid(particleCid);
          setCurrentMarkdown(particle);
          updateSearchParams(createSearchParams({ cid: particleCid }), { replace: true });
        });
      }
    }
  }, [searchParams]);

  useEffect(() => {
    if (content.length && !currentMarkdown.length) {
      setCurrentMarkdown(content);
    }
  }, [content, currentMarkdown]);

  const handleSaveMarkdown = useCallback(
    debounce((markdown: string) => {
      addIfpsMessageOrCid(markdown, { ipfsApi }).then((cid) => {
        updateSearchParams(createSearchParams({ cid }), { replace: true });
        setLastCid(cid);

        setAdviser('๐Ÿ“ Particle saved to ipfs', 'yellow');

        setTimeout(() => {
          setAdviser('you can create content');
        }, 5000);
      });
    }, 5000),
    []
  );

  const saveMarkdown = useCallback(
    (markdown: string) => {
      setCurrentMarkdown(markdown);

      if (!firstEffectOccured.current || !ipfsApi) {
        return;
      }

      if (markdown.length === 0) {
        updateSearchParams(createSearchParams({}), { replace: true });
        setLastCid(undefined);
        return;
      }

      handleSaveMarkdown(markdown);
    },
    [handleSaveMarkdown, ipfsApi, updateSearchParams]
  );

  const addKeywords = useCallback(
    (type: 'from' | 'to', newItem: KeywordsItem[]) => {
      if (!isIpfsInitialized) {
        return;
      }

      const stateKeywords = type === 'from' ? keywordsFrom : keywordsTo;
      const setStateKeywords = type === 'from' ? setKeywordsFrom : setKeywordsTo;

      const uniqueArray = [
        ...new Map([...stateKeywords, ...newItem].map((item) => [item.cid, item])).values(),
      ];

      if (uniqueArray.length) {
        setStateKeywords(uniqueArray);
      }
    },
    [keywordsFrom, keywordsTo, isIpfsInitialized]
  );

  const removeKeywords = useCallback(
    (type: 'from' | 'to', itemCid: string) => {
      const stateKeywords = type === 'from' ? keywordsFrom : keywordsTo;
      const setStateKeywords = type === 'from' ? setKeywordsFrom : setKeywordsTo;
      const newState = stateKeywords.filter((item) => item.cid !== itemCid);
      setStateKeywords(newState);
    },
    [keywordsFrom, keywordsTo]
  );

  const contextValue = useMemo(
    () => ({
      currentMarkdown,
      loadedMarkdown: content,
      stateActionBar,
      keywordsFrom,
      keywordsTo,
      lastCid,
      setStateActionBar,
      addKeywords,
      removeKeywords,
      saveMarkdown,
    }),
    [
      stateActionBar,
      keywordsFrom,
      keywordsTo,
      lastCid,
      addKeywords,
      currentMarkdown,
      content,
      removeKeywords,
      saveMarkdown,
    ]
  );

  return <StudioContext.Provider value={contextValue}>{children}</StudioContext.Provider>;
}

export default StudioContextProvider;

Neighbours