date-fns#formatDistance TypeScript Examples

The following examples show how to use date-fns#formatDistance. You can vote up the ones you like or vote down the ones you don't like, and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: PostDate.tsx    From norfolkdevelopers-website with MIT License 6 votes vote down vote up
export default function PostDate({ date }: PostDateProps) {
  const daysSincePosted = differenceInCalendarDays(new Date(), date);

  if (daysSincePosted > 7) {
    return <>on {dateFormat(date)}</>
  }

  return <span title={dateFormat(date)}>{formatDistance(date, new Date())} ago</span>
}
Example #2
Source File: Releases.tsx    From frontend with Apache License 2.0 6 votes vote down vote up
Releases: FunctionComponent<Props> = ({ releases }) => {
  const { t, i18n } = useTranslation()
  const latestRelease = releases ? releases[0] : null

  return (
    <>
      {releases && releases.length > 0 && (
        <div className={styles.releases}>
          {latestRelease && (
            <div className={styles.releaseDetails}>
              <header>
                <h3>{t('changes-in-version', { "version-number": latestRelease.version })}</h3>
                <div>
                  {latestRelease.timestamp &&
                    formatDistance(
                      new Date(latestRelease.timestamp * 1000),
                      new Date(),
                      { addSuffix: true, locale: getLocale(i18n.language) }
                    )}
                </div>
              </header>
              <div
                dangerouslySetInnerHTML={{
                  __html: latestRelease.description ?? t('no-changelog-provided'),
                }}
              />
            </div>
          )}
        </div>
      )}
    </>
  )
}
Example #3
Source File: Transactions.tsx    From solana-pay with Apache License 2.0 6 votes vote down vote up
Transaction: FC<{ transaction: Transaction }> = ({ transaction }) => {
    const { icon, symbol } = useConfig();

    const amount = useMemo(() => new BigNumber(transaction.amount), [transaction.amount]);
    const signature = useMemo(
        () => transaction.signature.slice(0, 8) + '....' + transaction.signature.slice(-8),
        [transaction.signature]
    );

    const getTime = useCallback(
        () => formatDistance(new Date(), new Date(transaction.timestamp * 1000)) + ' ago',
        [transaction.timestamp]
    );
    const [time, setTime] = useState(getTime());
    useEffect(() => {
        const interval = setInterval(() => setTime(getTime()), 1000);
        return () => clearInterval(interval);
    }, [getTime]);

    return (
        <div className={css.transaction}>
            <div className={css.icon}>{icon}</div>
            <div className={css.left}>
                <div className={css.amount}>
                    <Amount amount={amount} showZero />
                    {NON_BREAKING_SPACE + symbol}
                </div>
                <div className={css.signature}>{signature}</div>
            </div>
            <div className={css.right}>
                <div className={css.time}>{time}</div>
                <div className={clsx(css.status, css[`status-${transaction.status}`])}>{transaction.status}</div>
            </div>
        </div>
    );
}
Example #4
Source File: Sidebartem.tsx    From wiregui with MIT License 5 votes vote down vote up
export default function SidebarItem({
  name,
  address,
  lastConnectAt,
  active,
  selected,
}: SideBarParams) {
  return (
    <ContextMenuTrigger
      menuId={`menu-${name}`}
      passData={{
        name,
      }}
    >
      <Flex
        bg={selected ? "whiteAlpha.100" : ""}
        align="center"
        cursor="pointer"
        w="100%"
        p="4"
        transition=".3s ease"
        _hover={{ background: "whiteAlpha.200" }}
      >
        <Box
          bg={active ? "green.400" : "whiteAlpha.600"}
          w="10px"
          h="10px"
          borderRadius="50%"
        />
        <Flex direction="column" ml="2" w="89%">
          <Text
            color="whiteAlpha.700"
            w="100%"
            whiteSpace="nowrap"
            overflow="hidden"
            textOverflow="ellipsis"
          >
            {name}
          </Text>
          <Flex color="whiteAlpha.600" justify="space-between">
            {address && <Text fontSize="xs">{address[0]}</Text>}
            <Text fontSize="xs">
              {lastConnectAt
                ? formatDistance(new Date(lastConnectAt), new Date(), {
                    addSuffix: true,
                  })
                : "never"}
            </Text>
          </Flex>
        </Flex>
      </Flex>
    </ContextMenuTrigger>
  );
}
Example #5
Source File: aws-sso-integration-service.ts    From leapp with Mozilla Public License 2.0 5 votes vote down vote up
remainingHours(integration: AwsSsoIntegration): string {
    return formatDistance(new Date(integration.accessTokenExpiration), this.getDate(), { addSuffix: true });
  }
Example #6
Source File: EditorPanel.tsx    From vscode-crossnote with GNU Affero General Public License v3.0 4 votes vote down vote up
export default function EditorPanel(props: Props) {
  const classes = useStyles(props);
  const { t } = useTranslation();
  const [textAreaElement, setTextAreaElement] = useState<HTMLTextAreaElement>(
    null
  );
  const [previewElement, setPreviewElement] = useState<HTMLElement>(null);
  const [editor, setEditor] = useState<CodeMirrorEditor>(null);
  const [cursorPosition, setCursorPosition] = useState<CursorPosition>({
    line: 0,
    ch: 0,
  });
  const [note, setNote] = useState<Note>(null);
  const [editorMode, setEditorMode] = useState<EditorMode>(EditorMode.Preview);
  const [deleteDialogOpen, setDeleteDialogOpen] = useState<boolean>(false);
  const [filePathDialogOpen, setFilePathDialogOpen] = useState<boolean>(false);
  const [toggleEncryptionDialogOpen, setToggleEncryptionDialogOpen] = useState<
    boolean
  >(false);
  const [toggleEncryptionPassword, setToggleEncryptionPassword] = useState<
    string
  >("");
  const [decryptionDialogOpen, setDecryptionDialogOpen] = useState<boolean>(
    false
  );
  const [decryptionPassword, setDecryptionPassword] = useState<string>("");
  const [isDecrypted, setIsDecrypted] = useState<boolean>(false);
  const [tagsMenuAnchorEl, setTagsMenuAnchorEl] = useState<HTMLElement>(null);
  const [tagNames, setTagNames] = useState<string[]>([]);
  const [editImageElement, setEditImageElement] = useState<HTMLImageElement>(
    null
  );
  const [editImageTextMarker, setEditImageTextMarker] = useState<TextMarker>(
    null
  );
  const [editImageDialogOpen, setEditImageDialogOpen] = useState<boolean>(
    false
  );
  const [previewIsPresentation, setPreviewIsPresentation] = useState<boolean>(
    false
  );
  const [notebookTagNode, setNotebookTagNode] = useState<TagNode>(null);
  const [forceUpdate, setForceUpdate] = useState<number>(Date.now());

  const updateNote = useCallback(
    (note: Note, markdown: string, password: string = "") => {
      vscode.postMessage({
        action: MessageAction.UpdateNote,
        data: {
          note,
          markdown,
          password,
        },
      });
    },
    []
  );

  const closeFilePathDialog = useCallback(() => {
    if (!note) {
      return;
    }
    setFilePathDialogOpen(false);
  }, [note]);

  const closeEncryptionDialog = useCallback(() => {
    setToggleEncryptionPassword("");
    setToggleEncryptionDialogOpen(false);
  }, []);

  const closeDecryptionDialog = useCallback(() => {
    setDecryptionPassword("");
    setDecryptionDialogOpen(false);
  }, []);

  const togglePin = useCallback(() => {
    if (note && editor && isDecrypted) {
      note.config.pinned = !note.config.pinned;
      if (!note.config.pinned) {
        delete note.config.pinned;
      }
      updateNote(
        note,
        editor.getValue(),
        note.config.encryption ? decryptionPassword : ""
      );
      setForceUpdate(Date.now());
    }
  }, [note, editor, decryptionPassword, isDecrypted]);

  const addTag = useCallback(
    (tagName: string) => {
      let tag = tagName.trim() || "";
      if (!note || !tag.length || !editor || !isDecrypted) {
        return;
      }
      tag = tag
        .replace(/\s+/g, " ")
        .replace(TagStopRegExp, "")
        .split("/")
        .map((t) => t.trim())
        .filter((x) => x.length > 0)
        .join("/");
      if (!tag.length) {
        return;
      }
      setTagNames((tagNames) => {
        const newTagNames =
          tagNames.indexOf(tag) >= 0 ? [...tagNames] : [tag, ...tagNames];
        note.config.tags = newTagNames.sort((x, y) => x.localeCompare(y));
        updateNote(
          note,
          editor.getValue(),
          note.config.encryption ? decryptionPassword : ""
        );
        // crossnoteContainer.updateNotebookTagNode();
        return newTagNames;
      });
    },
    [note, editor, decryptionPassword, isDecrypted]
  );

  const deleteTag = useCallback(
    (tagName: string) => {
      if (note && editor && isDecrypted) {
        setTagNames((tagNames) => {
          const newTagNames = tagNames.filter((t) => t !== tagName);
          note.config.tags = newTagNames.sort((x, y) => x.localeCompare(y));
          updateNote(
            note,
            editor.getValue(),
            note.config.encryption ? decryptionPassword : ""
          );
          // crossnoteContainer.updateNotebookTagNode();
          return newTagNames;
        });
      }
    },
    [note, editor, decryptionPassword, isDecrypted]
  );

  const toggleEncryption = useCallback(() => {
    if (!note || !editor) {
      return;
    }
    const markdown = editor.getValue();
    if (note.config.encryption) {
      // Disable encryption
      // Check if the password is correct
      try {
        const bytes = CryptoJS.AES.decrypt(
          note.markdown.trim(),
          toggleEncryptionPassword
        );
        const json = JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
        // Disable encryption
        note.config.encryption = null;
        delete note.config.encryption;
        updateNote(note, json.markdown, "");
        setDecryptionPassword("");
        setIsDecrypted(true);
        closeEncryptionDialog();
        // editor.setValue(json.markdown);
        editor.setOption("readOnly", false);
      } catch (error) {
        new Noty({
          type: "error",
          text: t("error/failed-to-disable-encryption"),
          layout: "topRight",
          theme: "relax",
          timeout: 5000,
        }).show();
      }
    } else {
      // Enable encryption
      note.config.encryption = {
        title: getHeaderFromMarkdown(markdown),
      };
      updateNote(note, editor.getValue(), toggleEncryptionPassword);
      setDecryptionPassword(toggleEncryptionPassword);
      setIsDecrypted(true);
      closeEncryptionDialog();
    }
  }, [note, editor, closeEncryptionDialog, toggleEncryptionPassword]);

  const decryptNote = useCallback(() => {
    if (!note || !editor) {
      return;
    }

    // Decrypt
    try {
      const bytes = CryptoJS.AES.decrypt(
        note.markdown.trim(),
        decryptionPassword
      );
      const json = JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
      editor.setOption("readOnly", false);
      editor.setValue(json.markdown);
      setIsDecrypted(true);
      setDecryptionDialogOpen(false); // Don't clear decryptionPassword

      if (json.markdown.length === 0) {
        setEditorMode(EditorMode.VickyMD);
      } else {
        setEditorMode(EditorMode.Preview);
      }
    } catch (error) {
      new Noty({
        type: "error",
        text: t("error/decryption-failed"),
        layout: "topRight",
        theme: "relax",
        timeout: 5000,
      }).show();
      setIsDecrypted(false);
    }
  }, [note, editor, decryptionPassword, closeDecryptionDialog]);

  const duplicateNote = useCallback(() => {
    if (!note) {
      return;
    }
    const message: Message = {
      action: MessageAction.DuplicateNote,
      data: note,
    };
    vscode.postMessage(message);
  }, [note]);

  const postprocessPreview = useCallback(
    (previewElement: HTMLElement) => {
      previewPostprocessPreview(previewElement, note, (flag) => {
        setPreviewIsPresentation(flag);
      });
    },
    [note]
  );
  useEffect(() => {
    const message: Message = {
      action: MessageAction.InitializedEditorPanelWebview,
      data: {},
    };
    vscode.postMessage(message);

    return () => {
      setEditor(null);
      setTextAreaElement(null);
      setPreviewElement(null);
    };
  }, []);

  useEffect(() => {
    const onMessage = (event) => {
      const message: Message = event.data;
      switch (message.action) {
        case MessageAction.SendNote:
          setNote(message.data);
          break;
        case MessageAction.UpdatedNote:
          setNote(message.data);
          break;
        case MessageAction.SendNotebookTagNode:
          setNotebookTagNode(message.data);
        default:
          break;
      }
    };
    window.addEventListener("message", onMessage);
    return () => {
      window.removeEventListener("message", onMessage);
    };
  }, []);

  useEffect(() => {
    if (textAreaElement && !editor) {
      // console.log("textarea element mounted");
      const editor: CodeMirrorEditor = VickyMD.fromTextArea(textAreaElement, {
        mode: {
          name: "hypermd",
          hashtag: true,
        },
        hmdFold: HMDFold,
        keyMap: crossnoteSettings.keyMap,
        showCursorWhenSelecting: true,
        inputStyle: "contenteditable",
        hmdClick: (info: any, cm: CodeMirrorEditor) => {
          let { text, url } = info;
          if (info.type === "link" || info.type === "url") {
            const footnoteRef = text.match(/\[[^[\]]+\](?:\[\])?$/); // bare link, footref or [foot][] . assume no escaping char inside
            if (!footnoteRef && (info.ctrlKey || info.altKey) && url) {
              // just open URL
              openURL(url, note);
              return false; // Prevent default click event
            }
          }
        },
      });
      editor.setOption("lineNumbers", false);
      editor.setOption("foldGutter", false);
      editor.setValue("");
      editor.on("cursorActivity", (instance) => {
        const cursor = instance.getCursor();
        if (cursor) {
          setCursorPosition({
            line: cursor.line,
            ch: cursor.ch,
          });
        }
      });
      setEditor(editor);
      initMathPreview(editor);
    }
  }, [textAreaElement, editor]);

  useEffect(() => {
    if (editor) {
      setTheme({
        editor,
        themeName: selectedTheme.name,
        baseUri:
          extensionPath +
          (extensionPath.endsWith("/") ? "" : "/") +
          "node_modules/vickymd/theme/",
      });
    }
  }, [editor]);

  // Initialize cursor color
  useEffect(() => {
    const styleID = "codemirror-cursor-style";
    let style = document.getElementById(styleID);
    if (!style) {
      style = document.createElement("style");
      style.id = styleID;
      document.body.appendChild(style);
    }
    style.innerText = `
    .CodeMirror-cursor.CodeMirror-cursor {
      border-left: 2px solid rgba(74, 144, 226, 1);
    }    
    `;
  }, []);

  /*
  useEffect(() => {
    if (note && editor) {
      editor.setValue(note.markdown);
      editor.refresh();
    }
  }, [note, editor]);
  */

  // Decryption
  useEffect(() => {
    if (!editor || !note) {
      return;
    }
    if (note.config.encryption) {
      setIsDecrypted(false);
      setDecryptionPassword("");
      editor.setOption("readOnly", true);
      editor.setValue(`? ${t("general/encrypted")}`);
      setDecryptionDialogOpen(true);
    } else {
      setIsDecrypted(true);
      setDecryptionPassword("");
      editor.setOption("readOnly", false);
      editor.setValue(note.markdown);
      setDecryptionDialogOpen(false);

      if (note.markdown.length === 0) {
        setEditorMode(EditorMode.VickyMD);
      } else {
        setEditorMode(EditorMode.Preview);
      }
    }
  }, [editor, note]);

  useEffect(() => {
    if (!editor || !note) {
      return;
    }
    setTagNames(note.config.tags || []);
    const changesHandler = () => {
      if (editor.getOption("readOnly") || !isDecrypted) {
        // This line is necessary for decryption...
        return;
      }
      const markdown = editor.getValue();

      if (!note.config.encryption && markdown === note.markdown) {
        return;
      }
      updateNote(
        note,
        markdown,
        note.config.encryption ? decryptionPassword : ""
      );
      setTagNames(note.config.tags || []); // After resolve conflicts
    };
    editor.on("changes", changesHandler);

    const keyupHandler = () => {
      if (!isDecrypted && note.config.encryption) {
        setDecryptionDialogOpen(true);
      }
    };
    editor.on("keyup", keyupHandler);

    const imageClickedHandler = (args: any) => {
      const marker: TextMarker = args.marker;
      const imageElement: HTMLImageElement = args.element;
      imageElement.setAttribute(
        "data-marker-position",
        JSON.stringify(marker.find())
      );
      setEditImageElement(imageElement);
      setEditImageTextMarker(marker);
      setEditImageDialogOpen(true);
    };
    editor.on("imageClicked", imageClickedHandler);

    const loadImage = (args: any) => {
      const element = args.element;
      const imageSrc = element.getAttribute("data-src");
      element.setAttribute("src", resolveNoteImageSrc(note, imageSrc));
    };
    editor.on("imageReadyToLoad", loadImage);

    return () => {
      editor.off("changes", changesHandler);
      editor.off("keyup", keyupHandler);
      editor.off("imageClicked", imageClickedHandler);
      editor.off("imageReadyToLoad", loadImage);
    };
  }, [editor, note, decryptionPassword, isDecrypted]);

  useEffect(() => {
    if (!editor || !note) {
      return;
    }
    if (editorMode === EditorMode.VickyMD) {
      VickyMD.switchToHyperMD(editor);
      // @ts-ignore
      editor.setOption("hmdFold", HMDFold);
      editor.getWrapperElement().style.display = "block";
      editor.refresh();
    } else if (editorMode === EditorMode.SourceCode) {
      VickyMD.switchToNormal(editor);
      editor.getWrapperElement().style.display = "block";
      editor.refresh();
    } else if (editorMode === EditorMode.Preview) {
      editor.getWrapperElement().style.display = "none";
    } else if (editorMode === EditorMode.SplitView) {
      VickyMD.switchToNormal(editor);
      editor.getWrapperElement().style.display = "block";
      editor.refresh();
    }
  }, [editorMode, editor, note, isDecrypted]);

  // Render Preview
  useEffect(() => {
    if (
      (editorMode === EditorMode.Preview ||
        editorMode === EditorMode.SplitView) &&
      editor &&
      note &&
      previewElement
    ) {
      if (isDecrypted) {
        try {
          renderPreview(previewElement, editor.getValue());
          postprocessPreview(previewElement);
          previewElement.scrollTop = 0;
        } catch (error) {
          previewElement.innerText = error;
        }
      } else {
        previewElement.innerHTML = `? ${t("general/encrypted")}`;
        const clickHandler = () => {
          setDecryptionDialogOpen(true);
        };
        previewElement.addEventListener("click", clickHandler);
        return () => {
          previewElement.removeEventListener("click", clickHandler);
        };
      }
    }
  }, [
    editorMode,
    editor,
    previewElement,
    note,
    isDecrypted,
    postprocessPreview,
    t,
  ]);

  // Command
  useEffect(() => {
    if (!editor || !note) {
      return;
    }

    const onChangeHandler = (
      instance: CodeMirrorEditor,
      changeObject: EditorChangeLinkedList
    ) => {
      // Check commands
      if (changeObject.text.length === 1 && changeObject.text[0] === "/") {
        const aheadStr = editor
          .getLine(changeObject.from.line)
          .slice(0, changeObject.from.ch + 1);
        if (!aheadStr.match(/#[^\s]+?\/$/)) {
          // Not `/` inside a tag
          // @ts-ignore
          editor.showHint({
            closeOnUnfocus: false,
            completeSingle: false,
            hint: () => {
              const cursor = editor.getCursor();
              const token = editor.getTokenAt(cursor);
              const line = cursor.line;
              const lineStr = editor.getLine(line);
              const end: number = cursor.ch;
              let start = token.start;
              if (lineStr[start] !== "/") {
                start = start - 1;
              }
              const currentWord: string = lineStr
                .slice(start, end)
                .replace(/^\//, "");

              const render = (
                element: HTMLElement,
                data: CommandHint[],
                cur: CommandHint
              ) => {
                const wrapper = document.createElement("div");
                wrapper.style.padding = "6px 0";
                wrapper.style.display = "flex";
                wrapper.style.flexDirection = "row";
                wrapper.style.alignItems = "flex-start";
                wrapper.style.maxWidth = "100%";
                wrapper.style.minWidth = "200px";

                const leftPanel = document.createElement("div");
                const iconWrapper = document.createElement("div");
                iconWrapper.style.padding = "0 6px";
                iconWrapper.style.marginRight = "6px";
                iconWrapper.style.fontSize = "1rem";

                const iconElement = document.createElement("span");
                iconElement.classList.add("mdi");
                iconElement.classList.add(
                  cur.icon || "mdi-help-circle-outline"
                );
                iconWrapper.appendChild(iconElement);
                leftPanel.appendChild(iconWrapper);

                const rightPanel = document.createElement("div");

                const descriptionElement = document.createElement("p");
                descriptionElement.innerText = cur.description;
                descriptionElement.style.margin = "2px 0";
                descriptionElement.style.padding = "0";

                const commandElement = document.createElement("p");
                commandElement.innerText = cur.command;
                commandElement.style.margin = "0";
                commandElement.style.padding = "0";
                commandElement.style.fontSize = "0.7rem";

                rightPanel.appendChild(descriptionElement);
                rightPanel.appendChild(commandElement);

                wrapper.appendChild(leftPanel);
                wrapper.appendChild(rightPanel);
                element.appendChild(wrapper);
              };

              const commands: CommandHint[] = [
                {
                  text: "# ",
                  command: "/h1",
                  description: t("editor/toolbar/insert-header-1"),
                  icon: "mdi-format-header-1",
                  render,
                },
                {
                  text: "## ",
                  command: "/h2",
                  description: t("editor/toolbar/insert-header-2"),
                  icon: "mdi-format-header-2",
                  render,
                },
                {
                  text: "### ",
                  command: "/h3",
                  description: t("editor/toolbar/insert-header-3"),
                  icon: "mdi-format-header-3",
                  render,
                },
                {
                  text: "#### ",
                  command: "/h4",
                  description: t("editor/toolbar/insert-header-4"),
                  icon: "mdi-format-header-4",
                  render,
                },
                {
                  text: "##### ",
                  command: "/h5",
                  description: t("editor/toolbar/insert-header-5"),
                  icon: "mdi-format-header-5",
                  render,
                },
                {
                  text: "###### ",
                  command: "/h6",
                  description: t("editor/toolbar/insert-header-6"),
                  icon: "mdi-format-header-6",
                  render,
                },
                {
                  text: "> ",
                  command: "/blockquote",
                  description: t("editor/toolbar/insert-blockquote"),
                  icon: "mdi-format-quote-open",
                  render,
                },
                {
                  text: "* ",
                  command: "/ul",
                  description: t("editor/toolbar/insert-unordered-list"),
                  icon: "mdi-format-list-bulleted",
                  render,
                },
                {
                  text: "1. ",
                  command: "/ol",
                  description: t("editor/toolbar/insert-ordered-list"),
                  icon: "mdi-format-list-numbered",
                  render,
                },
                {
                  text: "<!-- @crossnote.image -->\n",
                  command: "/image",
                  description: t("editor/toolbar/insert-image"),
                  icon: "mdi-image",
                  render,
                },
                {
                  text: `|   |   |
  |---|---|
  |   |   |
  `,
                  command: "/table",
                  description: t("editor/toolbar/insert-table"),
                  icon: "mdi-table",
                  render,
                },
                {
                  text:
                    "<!-- @timer " +
                    JSON.stringify({ date: new Date().toString() })
                      .replace(/^{/, "")
                      .replace(/}$/, "") +
                    " -->\n",
                  command: "/timer",
                  description: t("editor/toolbar/insert-clock"),
                  icon: "mdi-timer",
                  render,
                },
                {
                  text: "<!-- @crossnote.audio -->  \n",
                  command: "/audio",
                  description: t("editor/toolbar/insert-audio"),
                  icon: "mdi-music",
                  render,
                },
                /*
                  {
                    text: "<!-- @crossnote.netease_music -->  \n",
                    displayText: `/netease - ${t(
                      "editor/toolbar/netease-music",
                    )}`,
                  },
                  */
                {
                  text: "<!-- @crossnote.video -->  \n",
                  command: "/video",
                  description: t("editor/toolbar/insert-video"),
                  icon: "mdi-video",
                  render,
                },
                {
                  text: "<!-- @crossnote.youtube -->  \n",
                  command: "/youtube",
                  description: t("editor/toolbar/insert-youtube"),
                  icon: "mdi-youtube",
                  render,
                },
                {
                  text: "<!-- @crossnote.bilibili -->  \n",
                  command: "/bilibili",
                  description: t("editor/toolbar/insert-bilibili"),
                  icon: "mdi-television-classic",
                  render,
                },
                {
                  text: "<!-- slide -->  \n",
                  command: "/slide",
                  description: t("editor/toolbar/insert-slide"),
                  icon: "mdi-presentation",
                  render,
                },
                {
                  text: "<!-- @crossnote.ocr -->  \n",
                  command: "/ocr",
                  description: t("editor/toolbar/insert-ocr"),
                  icon: "mdi-ocr",
                  render,
                },
                {
                  text:
                    '<!-- @crossnote.kanban "v":2,"board":{"columns":[]} -->  \n',
                  command: "/kanban",
                  description: `${t("editor/toolbar/insert-kanban")} (beta)`,
                  icon: "mdi-developer-board",
                  render,
                },
                /*
                  {
                    text: "<!-- @crossnote.abc -->  \n",
                    displayText: `/abc - ${t(
                      "editor/toolbar/insert-abc-notation",
                    )}`,
                  },
                  */
                {
                  text: "<!-- @crossnote.github_gist -->  \n",
                  command: "/github_gist",
                  description: t("editor/toolbar/insert-github-gist"),
                  icon: "mdi-github",
                  render,
                },
                /*
                {
                  text: "<!-- @crossnote.comment -->  \n",
                  command: "/crossnote.comment",
                  description: t("editor/toolbar/insert-comment"),
                  icon: "mdi-comment-multiple",
                  render,
                },
                */
              ];
              const filtered = commands.filter(
                (item) =>
                  (item.command + item.description)
                    .toLocaleLowerCase()
                    .indexOf(currentWord.toLowerCase()) >= 0
              );
              return {
                list: filtered.length ? filtered : commands,
                from: { line, ch: start },
                to: { line, ch: end },
              };
            },
          });
        }
      }

      // Check emoji
      if (
        changeObject.text.length === 1 &&
        changeObject.text[0].length > 0 &&
        changeObject.text[0] !== " " &&
        changeObject.text[0] !== ":" &&
        changeObject.from.ch > 0 &&
        editor.getLine(changeObject.from.line)[changeObject.from.ch - 1] === ":"
      ) {
        // @ts-ignore
        editor.showHint({
          closeOnUnfocus: true,
          completeSingle: false,
          hint: () => {
            const cursor = editor.getCursor();
            const token = editor.getTokenAt(cursor);
            const line = cursor.line;
            const lineStr = editor.getLine(line);
            const end: number = cursor.ch;
            let start = token.start;
            let doubleSemiColon = false;
            if (lineStr[start] !== ":") {
              start = start - 1;
            }
            if (start > 0 && lineStr[start - 1] === ":") {
              start = start - 1;
              doubleSemiColon = true;
            }
            const currentWord: string = lineStr
              .slice(start, end)
              .replace(/^:+/, "");

            const commands: { text: string; displayText: string }[] = [];
            for (const def in EmojiDefinitions) {
              const emoji = EmojiDefinitions[def];
              commands.push({
                text: doubleSemiColon ? `:${def}: ` : `${emoji} `,
                displayText: `:${def}: ${emoji}`,
              });
            }
            const filtered = commands.filter(
              (item) =>
                item.displayText
                  .toLocaleLowerCase()
                  .indexOf(currentWord.toLowerCase()) >= 0
            );
            return {
              list: filtered.length ? filtered : commands,
              from: { line, ch: start },
              to: { line, ch: end },
            };
          },
        });
      }

      // Check tag
      if (
        changeObject.text.length === 1 &&
        changeObject.text[0] !== " " &&
        changeObject.from.ch > 0 &&
        editor.getLine(changeObject.from.line)[changeObject.from.ch - 1] === "#"
      ) {
        // @ts-ignore
        editor.showHint({
          closeOnUnfocus: true,
          completeSingle: false,
          hint: () => {
            const cursor = editor.getCursor();
            const token = editor.getTokenAt(cursor);
            const line = cursor.line;
            const lineStr = editor.getLine(line);
            const end: number = cursor.ch;
            let start = token.start;
            if (lineStr[start] !== "#") {
              start = start - 1;
            }
            const currentWord: string = lineStr
              .slice(start, end)
              .replace(TagStopRegExp, "");
            const commands: { text: string; displayText: string }[] = [];
            if (currentWord.trim().length > 0) {
              commands.push({
                text: `#${currentWord} `,
                displayText: `+ #${currentWord}`,
              });
            }
            const helper = (children: TagNode[]) => {
              if (!children || !children.length) {
                return;
              }
              for (let i = 0; i < children.length; i++) {
                const tag = children[i].path;
                commands.push({
                  text: `#${tag} `,
                  displayText: `+ #${tag}`,
                });
                helper(children[i].children);
              }
            };
            helper(notebookTagNode?.children || []);
            const filtered = commands.filter(
              (item) => item.text.toLocaleLowerCase().indexOf(currentWord) >= 0
            );

            return {
              list: filtered.length ? filtered : commands,
              from: { line, ch: start },
              to: { line, ch: end },
            };
          },
        });
      }

      // Timer
      if (
        changeObject.text.length > 0 &&
        changeObject.text[0].startsWith("<!-- @timer ") &&
        changeObject.removed.length > 0 &&
        changeObject.removed[0].startsWith("/")
      ) {
        // Calcuate date time
        const lines = editor.getValue().split("\n");
        const timerTexts: TimerText[] = [];
        for (let i = 0; i < lines.length; i++) {
          const match = lines[i].match(/^`@timer\s.+`/);
          if (match) {
            const text = match[0];
            const dataMatch = text.match(/^`@timer\s+(.+)`/);
            if (!dataMatch || !dataMatch.length) {
              continue;
            }
            const dataStr = dataMatch[1];
            try {
              const data = JSON.parse(`{${dataStr}}`);
              const datetime = data["date"];
              if (datetime) {
                timerTexts.push({
                  text: text,
                  line: i,
                  date: new Date(datetime),
                });
              }
            } catch (error) {
              continue;
            }
          }
        }
        for (let i = 1; i < timerTexts.length; i++) {
          const currentTimerText = timerTexts[i];
          const previousTimerText = timerTexts[i - 1];
          const duration = formatDistance(
            currentTimerText.date,
            previousTimerText.date,
            { includeSeconds: true }
          );
          const newText = `\`@timer ${JSON.stringify({
            date: currentTimerText.date.toString(),
            duration,
          })
            .replace(/^{/, "")
            .replace(/}$/, "")}\``;
          editor.replaceRange(
            newText,
            { line: currentTimerText.line, ch: 0 },
            { line: currentTimerText.line, ch: currentTimerText.text.length }
          );
        }
      }

      // Add Tag
      if (
        changeObject.origin === "complete" &&
        changeObject.removed[0] &&
        changeObject.removed[0].match(/^#[^\s]/) &&
        changeObject.text[0] &&
        changeObject.text[0].match(/^#[^\s]/)
      ) {
        addTag(changeObject.text[0].replace(/^#/, ""));
      }
    };
    editor.on("change", onChangeHandler);

    const onCursorActivityHandler = (instance: CodeMirrorEditor) => {
      // console.log("cursorActivity", editor.getCursor());
      // console.log("selection: ", editor.getSelection());
      return;
    };
    editor.on("cursorActivity", onCursorActivityHandler);

    return () => {
      editor.off("change", onChangeHandler);
      editor.off("cursorActivity", onCursorActivityHandler);
    };
  }, [editor, note, notebookTagNode, addTag /*t*/]);

  // Split view
  useEffect(() => {
    if (
      !editor ||
      !note ||
      !previewElement ||
      editorMode !== EditorMode.SplitView
    ) {
      return;
    }
    let onChangeCallback: any = null;
    let onCursorActivityCallback: any = null;
    let onScrollCallback: any = null;
    let onWindowResizeCallback: any = null;
    let scrollMap: any = null;
    let scrollTimeout: NodeJS.Timeout = null;
    let previewScrollDelay = Date.now();
    let editorScrollDelay = Date.now();
    let currentLine: number = -1;
    let editorClientWidth = editor.getScrollInfo().clientWidth;
    let editorClientHeight = editor.getScrollInfo().clientHeight;
    let lastCursorPosition: Position = null;

    const totalLineCount = editor.lineCount();
    const buildScrollMap = () => {
      if (!totalLineCount) {
        return null;
      }
      const scrollMap = [];
      const nonEmptyList = [];

      for (let i = 0; i < totalLineCount; i++) {
        scrollMap.push(-1);
      }

      nonEmptyList.push(0);
      scrollMap[0] = 0;

      // write down the offsetTop of element that has 'data-line' property to scrollMap
      const lineElements = previewElement.getElementsByClassName("sync-line");

      for (let i = 0; i < lineElements.length; i++) {
        let el = lineElements[i] as HTMLElement;
        let t: any = el.getAttribute("data-line");
        if (!t) {
          continue;
        }

        t = parseInt(t, 10);
        if (!t) {
          continue;
        }

        // this is for ignoring footnote scroll match
        if (t < nonEmptyList[nonEmptyList.length - 1]) {
          el.removeAttribute("data-line");
        } else {
          nonEmptyList.push(t);

          let offsetTop = 0;
          while (el && el !== previewElement) {
            offsetTop += el.offsetTop;
            el = el.offsetParent as HTMLElement;
          }

          scrollMap[t] = Math.round(offsetTop);
        }
      }

      nonEmptyList.push(totalLineCount);
      scrollMap.push(previewElement.scrollHeight);

      let pos = 0;
      for (let i = 0; i < totalLineCount; i++) {
        if (scrollMap[i] !== -1) {
          pos++;
          continue;
        }

        const a = nonEmptyList[pos - 1];
        const b = nonEmptyList[pos];
        scrollMap[i] = Math.round(
          (scrollMap[b] * (i - a) + scrollMap[a] * (b - i)) / (b - a)
        );
      }

      return scrollMap; // scrollMap's length == screenLineCount (vscode can't get screenLineCount... sad)
    };
    const scrollToPos = (scrollTop: number) => {
      if (scrollTimeout) {
        clearTimeout(scrollTimeout);
        scrollTimeout = null;
      }

      if (scrollTop < 0) {
        return;
      }

      const delay = 10;

      const helper = (duration = 0) => {
        scrollTimeout = setTimeout(() => {
          if (duration <= 0) {
            previewScrollDelay = Date.now() + 500;
            previewElement.scrollTop = scrollTop;
            return;
          }

          const difference = scrollTop - previewElement.scrollTop;

          const perTick = (difference / duration) * delay;

          // disable preview onscroll
          previewScrollDelay = Date.now() + 500;

          previewElement.scrollTop += perTick;
          if (previewElement.scrollTop === scrollTop) {
            return;
          }

          helper(duration - delay);
        }, delay);
      };

      const scrollDuration = 120;
      helper(scrollDuration);
    };
    const scrollToRevealSourceLine = (line: number, topRatio = 0.372) => {
      if (line === currentLine) {
        return;
      } else {
        currentLine = line;
      }

      // disable preview onscroll
      previewScrollDelay = Date.now() + 500;

      /*
        if (presentationMode) {
          scrollSyncToSlide(line);
        } else {
          scrollSyncToLine(line, topRatio);
        }
        */
      scrollSyncToLine(line, topRatio);
    };
    const scrollSyncToLine = (line: number, topRatio: number = 0.372) => {
      if (!scrollMap) {
        scrollMap = buildScrollMap();
      }
      if (!scrollMap || line >= scrollMap.length) {
        return;
      }

      if (line + 1 === totalLineCount) {
        // last line
        scrollToPos(previewElement.scrollHeight);
      } else {
        /**
         * Since I am not able to access the viewport of the editor
         * I used `golden section` (0.372) here for scrollTop.
         */
        scrollToPos(
          Math.max(scrollMap[line] - previewElement.offsetHeight * topRatio, 0)
        );
      }
    };
    const revealEditorLine = (line: number) => {
      const scrollInfo = editor.getScrollInfo();
      editor.scrollIntoView({ line: line, ch: 0 }, scrollInfo.clientHeight / 2);
      editorScrollDelay = Date.now() + 500;
      if (
        scrollInfo.clientHeight !== editorClientHeight ||
        scrollInfo.clientWidth !== editorClientWidth
      ) {
        editorClientHeight = scrollInfo.clientHeight;
        editorClientWidth = scrollInfo.clientWidth;
        scrollMap = null;
      }
    };
    const previewSyncSource = () => {
      let scrollToLine;

      if (previewElement.scrollTop === 0) {
        // editorScrollDelay = Date.now() + 100
        scrollToLine = 0;

        revealEditorLine(scrollToLine);
        return;
      }

      const top = previewElement.scrollTop + previewElement.offsetHeight / 2;

      // try to find corresponding screen buffer row
      if (!scrollMap) {
        scrollMap = buildScrollMap();
      }

      let i = 0;
      let j = scrollMap.length - 1;
      let count = 0;
      let screenRow = -1; // the screenRow is the bufferRow in vscode.
      let mid;

      while (count < 20) {
        if (Math.abs(top - scrollMap[i]) < 20) {
          screenRow = i;
          break;
        } else if (Math.abs(top - scrollMap[j]) < 20) {
          screenRow = j;
          break;
        } else {
          mid = Math.floor((i + j) / 2);
          if (top > scrollMap[mid]) {
            i = mid;
          } else {
            j = mid;
          }
        }
        count++;
      }

      if (screenRow === -1) {
        screenRow = mid;
      }

      scrollToLine = screenRow;
      revealEditorLine(scrollToLine);
      // @scrollToPos(screenRow * @editor.getLineHeightInPixels() - @previewElement.offsetHeight / 2, @editor.getElement())
      // # @editor.getElement().setScrollTop

      // track currnet time to disable onDidChangeScrollTop
      // editorScrollDelay = Date.now() + 100
    };

    onChangeCallback = () => {
      try {
        const markdown = editor.getValue();
        setTimeout(() => {
          const newMarkdown = editor.getValue();
          if (markdown === newMarkdown) {
            renderPreview(previewElement, newMarkdown);
            postprocessPreview(previewElement);
          }
        }, 300);
      } catch (error) {
        previewElement.innerText = error;
      }
      // Reset scrollMap
      scrollMap = null;
    };
    onCursorActivityCallback = () => {
      const cursor = editor.getCursor();
      const scrollInfo = editor.getScrollInfo();
      const firstLine = editor.lineAtHeight(scrollInfo.top, "local");
      const lastLine = editor.lineAtHeight(
        scrollInfo.top + scrollInfo.clientHeight,
        "local"
      );
      if (!lastCursorPosition || lastCursorPosition.line !== cursor.line) {
        scrollSyncToLine(
          cursor.line,
          (cursor.line - firstLine) / (lastLine - firstLine)
        );
      }
      lastCursorPosition = cursor;
    };
    onScrollCallback = () => {
      // console.log("scroll editor: ", editor.getScrollInfo());
      // console.log("viewport: ", editor.getViewport());
      const scrollInfo = editor.getScrollInfo();
      if (
        scrollInfo.clientHeight !== editorClientHeight ||
        scrollInfo.clientWidth !== editorClientWidth
      ) {
        editorClientHeight = scrollInfo.clientHeight;
        editorClientWidth = scrollInfo.clientWidth;
        scrollMap = null;
      }

      if (Date.now() < editorScrollDelay) {
        return;
      }
      const topLine = editor.lineAtHeight(scrollInfo.top, "local");
      const bottomLine = editor.lineAtHeight(
        scrollInfo.top + scrollInfo.clientHeight,
        "local"
      );
      let midLine;
      if (topLine === 0) {
        midLine = 0;
      } else if (bottomLine === totalLineCount - 1) {
        midLine = bottomLine;
      } else {
        midLine = Math.floor((topLine + bottomLine) / 2);
      }
      scrollSyncToLine(midLine);
    };
    onWindowResizeCallback = () => {
      const scrollInfo = editor.getScrollInfo();
      editorClientHeight = scrollInfo.clientHeight;
      editorClientWidth = scrollInfo.clientWidth;
      scrollMap = null;
    };

    editor.on("changes", onChangeCallback);
    onChangeCallback();

    editor.on("cursorActivity", onCursorActivityCallback);
    editor.on("scroll", onScrollCallback);
    previewElement.onscroll = () => {
      // console.log("scroll preview: ", previewElement.scrollTop);
      if (Date.now() < previewScrollDelay) {
        return;
      }
      previewSyncSource();
    };
    window.addEventListener("resize", onWindowResizeCallback);

    return () => {
      if (onChangeCallback) {
        editor.off("changes", onChangeCallback);
      }
      if (onCursorActivityCallback) {
        editor.off("cursorActivity", onCursorActivityCallback);
      }
      if (onScrollCallback) {
        editor.off("scroll", onScrollCallback);
      }
      if (onWindowResizeCallback) {
        window.removeEventListener("resize", onWindowResizeCallback);
      }
    };
  }, [editor, note, previewElement, editorMode, postprocessPreview]);

  if (!note) {
    return (
      <Box className={clsx(classes.editorPanel, "editor-panel")}>
        <Box
          style={{
            margin: "0 auto",
            top: "50%",
            position: "relative",
          }}
        >
          <Typography>{`? ${t("general/no-notes-found")}`}</Typography>
        </Box>
      </Box>
    );
  }

  return (
    <Box className={clsx(classes.editorPanel)}>
      <Box className={clsx(classes.topPanel)}>
        <Box className={clsx(classes.row)}>
          <ButtonGroup
            variant={"outlined"}
            color="default"
            aria-label="editor mode"
          >
            <Tooltip title={t("general/vickymd")}>
              <Button
                className={clsx(
                  classes.controlBtn,
                  editorMode === EditorMode.VickyMD &&
                    classes.controlBtnSelected
                )}
                color={
                  editorMode === EditorMode.VickyMD ? "primary" : "default"
                }
                onClick={() => setEditorMode(EditorMode.VickyMD)}
              >
                <Pencil></Pencil>
              </Button>
            </Tooltip>
            <Tooltip title={t("editor/note-control/source-code")}>
              <Button
                className={clsx(
                  classes.controlBtn,
                  editorMode === EditorMode.SourceCode &&
                    classes.controlBtnSelected
                )}
                color={
                  editorMode === EditorMode.SourceCode ? "primary" : "default"
                }
                onClick={() => setEditorMode(EditorMode.SourceCode)}
              >
                <CodeTags></CodeTags>
              </Button>
            </Tooltip>
            <Tooltip title={t("editor/note-control/split-view")}>
              <Button
                className={clsx(
                  classes.controlBtn,
                  editorMode === EditorMode.SplitView &&
                    classes.controlBtnSelected
                )}
                color={
                  editorMode === EditorMode.SplitView ? "primary" : "default"
                }
                onClick={() => setEditorMode(EditorMode.SplitView)}
              >
                <ViewSplitVertical></ViewSplitVertical>
              </Button>
            </Tooltip>
            <Tooltip title={t("editor/note-control/preview")}>
              <Button
                className={clsx(
                  classes.controlBtn,
                  editorMode === EditorMode.Preview &&
                    classes.controlBtnSelected
                )}
                color={
                  editorMode === EditorMode.Preview ? "primary" : "default"
                }
                onClick={() => setEditorMode(EditorMode.Preview)}
              >
                <FilePresentationBox></FilePresentationBox>
              </Button>
            </Tooltip>
          </ButtonGroup>
          <ButtonGroup style={{ marginLeft: "8px" }}>
            {isDecrypted && note && (
              <Tooltip title={t("general/tags")}>
                <Button
                  className={clsx(
                    classes.controlBtn,
                    note.config.tags &&
                      note.config.tags.length > 0 &&
                      classes.controlBtnSelectedSecondary
                  )}
                  onClick={(event) => setTagsMenuAnchorEl(event.currentTarget)}
                >
                  {note.config.tags && note.config.tags.length > 0 ? (
                    <Tag></Tag>
                  ) : (
                    <TagOutline></TagOutline>
                  )}
                </Button>
              </Tooltip>
            )}
            {isDecrypted && note && (
              <Tooltip title={t("general/Pin")}>
                <Button
                  className={clsx(
                    classes.controlBtn,
                    note.config.pinned && classes.controlBtnSelectedSecondary
                  )}
                  onClick={togglePin}
                >
                  {note.config.pinned ? <Pin></Pin> : <PinOutline></PinOutline>}
                </Button>
              </Tooltip>
            )}
            {note && (
              <Tooltip title={t("general/Encryption")}>
                <Button
                  className={clsx(
                    classes.controlBtn,
                    note.config.encryption &&
                      classes.controlBtnSelectedSecondary
                  )}
                  onClick={() => setToggleEncryptionDialogOpen(true)}
                >
                  {note.config.encryption ? (
                    <Lock></Lock>
                  ) : (
                    <LockOpenOutline></LockOpenOutline>
                  )}
                </Button>
              </Tooltip>
            )}
          </ButtonGroup>
          <ButtonGroup style={{ marginLeft: "8px" }}>
            <Tooltip title={t("general/change-file-path")}>
              <Button
                className={clsx(classes.controlBtn)}
                onClick={() => setFilePathDialogOpen(true)}
              >
                <RenameBox></RenameBox>
              </Button>
            </Tooltip>
            <Tooltip title={t("general/Delete")}>
              <Button
                className={clsx(classes.controlBtn)}
                onClick={() => setDeleteDialogOpen(true)}
              >
                <Delete></Delete>
              </Button>
            </Tooltip>
            {note && !note.config.encryption && (
              <Tooltip title={t("general/create-a-copy")}>
                <Button
                  className={clsx(classes.controlBtn)}
                  onClick={duplicateNote}
                >
                  <ContentDuplicate></ContentDuplicate>
                </Button>
              </Tooltip>
            )}
          </ButtonGroup>
          <TagsMenuPopover
            anchorElement={tagsMenuAnchorEl}
            onClose={() => setTagsMenuAnchorEl(null)}
            addTag={addTag}
            deleteTag={deleteTag}
            tagNames={tagNames}
            notebookTagNode={notebookTagNode}
          ></TagsMenuPopover>
        </Box>
      </Box>
      <Box
        className={clsx(
          classes.editorWrapper,
          editorMode === EditorMode.SplitView ? classes.splitView : null
        )}
      >
        <textarea
          className={clsx(classes.editor, "editor-textarea")}
          placeholder={t("editor/placeholder")}
          ref={(element: HTMLTextAreaElement) => {
            setTextAreaElement(element);
          }}
        ></textarea>
        {(editorMode === EditorMode.Preview ||
          editorMode === EditorMode.SplitView) &&
        editor ? (
          <div
            className={clsx(
              classes.preview,
              "preview",
              previewIsPresentation ? classes.presentation : null
            )}
            ref={(element: HTMLElement) => {
              setPreviewElement(element);
            }}
          ></div>
        ) : null}
      </Box>
      <Box className={clsx(classes.bottomPanel, "editor-bottom-panel")}>
        {note && (
          <Box className={clsx(classes.row)}>
            <Breadcrumbs aria-label={"File path"} maxItems={4}>
              {note.filePath.split("/").map((path, offset, arr) => {
                return (
                  <Typography
                    variant={"caption"}
                    style={{ cursor: "pointer" }}
                    color={"textPrimary"}
                    key={`${offset}-${path}`}
                    onClick={() => {
                      if (offset === arr.length - 1) {
                        setFilePathDialogOpen(true);
                      } else {
                        setSelectedSection({
                          type: CrossnoteSectionType.Directory,
                          path: arr.slice(0, offset + 1).join("/"),
                          notebook: {
                            name: "",
                            dir: note.notebookPath,
                          },
                        });
                      }
                    }}
                  >
                    {path}
                  </Typography>
                );
              })}
            </Breadcrumbs>
          </Box>
        )}
        <Box className={clsx(classes.cursorPositionInfo)}>
          <Typography variant={"caption"} color={"textPrimary"}>
            {`Ln ${cursorPosition.line + 1}, Col ${cursorPosition.ch}`}
          </Typography>
        </Box>
      </Box>

      <Card
        id="math-preview"
        className={clsx(classes.floatWin, "float-win", "float-win-hidden")}
      >
        <Box className={clsx(classes.floatWinTitle, "float-win-title")}>
          <IconButton
            className={clsx(classes.floatWinClose, "float-win-close")}
          >
            <Close></Close>
          </IconButton>
          <Typography>{t("general/math-preview")}</Typography>
        </Box>
        <Box
          className={clsx(classes.floatWinContent, "float-win-content")}
          id="math-preview-content"
        ></Box>
      </Card>

      <DeleteDialog
        open={deleteDialogOpen}
        onClose={() => setDeleteDialogOpen(false)}
        note={note}
      ></DeleteDialog>
      <ChangeFilePathDialog
        note={note}
        open={filePathDialogOpen}
        onClose={closeFilePathDialog}
      ></ChangeFilePathDialog>
      <EditImageDialog
        open={editImageDialogOpen}
        onClose={() => setEditImageDialogOpen(false)}
        editor={editor}
        imageElement={editImageElement}
        marker={editImageTextMarker}
        note={note}
      ></EditImageDialog>

      <Dialog open={toggleEncryptionDialogOpen} onClose={closeEncryptionDialog}>
        <DialogTitle>
          {note.config.encryption
            ? t("general/disable-the-encryption-on-this-note")
            : t("general/encrypt-this-note-with-password")}
        </DialogTitle>
        <DialogContent>
          <TextField
            value={toggleEncryptionPassword}
            autoFocus={true}
            onChange={(event) =>
              setToggleEncryptionPassword(event.target.value)
            }
            onKeyUp={(event) => {
              if (event.which === 13) {
                toggleEncryption();
              }
            }}
            placeholder={t("general/Password")}
            type={"password"}
          ></TextField>
        </DialogContent>
        <DialogActions>
          <Button
            variant={"contained"}
            color={"primary"}
            onClick={toggleEncryption}
          >
            {note.config.encryption ? <Lock></Lock> : <LockOpen></LockOpen>}
            {note.config.encryption
              ? t("general/disable-encryption")
              : t("general/encrypt")}
          </Button>
          <Button onClick={closeEncryptionDialog}>{t("general/cancel")}</Button>
        </DialogActions>
      </Dialog>

      <Dialog open={decryptionDialogOpen} onClose={closeDecryptionDialog}>
        <DialogTitle>{t("general/decrypt-this-note")}</DialogTitle>
        <DialogContent>
          <TextField
            value={decryptionPassword}
            autoFocus={true}
            onChange={(event) => setDecryptionPassword(event.target.value)}
            placeholder={t("general/Password")}
            type={"password"}
            onKeyUp={(event) => {
              if (event.which === 13) {
                decryptNote();
              }
            }}
          ></TextField>
        </DialogContent>
        <DialogActions>
          <Button variant={"contained"} color={"primary"} onClick={decryptNote}>
            {t("general/decrypt")}
          </Button>
          <Button onClick={closeDecryptionDialog}>{t("general/cancel")}</Button>
        </DialogActions>
      </Dialog>
    </Box>
  );
}
Example #7
Source File: CommentItemModule.tsx    From bouncecode-cms with GNU General Public License v3.0 4 votes vote down vote up
function CommentItemModule({post, comment}: ICommentItemModule) {
  const [moreVert, setMoreVert] = React.useState(null);
  const openMoreVert = Boolean(moreVert);
  const toggleMoreVert = open => {
    return event => {
      if (open) {
        setMoreVert(event.currentTarget);
      } else {
        setMoreVert(null);
      }
    };
  };

  const [commentDrawer, setCommentDrawer] = React.useState(null);
  const toggleCommentDrawer = open => {
    return event => {
      if (
        event.type === 'keydown' &&
        (event.key === 'Tab' || event.key === 'Shift')
      ) {
        return;
      }

      setCommentDrawer(open);
    };
  };

  const [
    emotionMutation,
    {loading: emotionLoading},
  ] = useCommentEmotionMutation();
  const [
    undoEmotionMutation,
    {loading: undoEmotionLoading},
  ] = useCommentEmotionUndoMutation();
  const updateEmotion = emotion => {
    return () => {
      console.log(myEmotion);
      console.log(emotion);
      if (myEmotion === emotion.toLowerCase()) {
        undoEmotionMutation({
          variables: {
            where: {
              id: comment.id,
            },
          },
          refetchQueries: [
            {
              query: CommentMyEmotionDocument,
              variables: {where: {id: comment.id}},
            },
          ],
        });
      } else {
        emotionMutation({
          variables: {
            where: {
              id: comment.id,
            },
            data: {emotion},
          },
          refetchQueries: [
            {
              query: CommentMyEmotionDocument,
              variables: {where: {id: comment.id}},
            },
          ],
        });
      }
    };
  };

  const {
    data: myEmotionData,
    loading: myEmotionLoading,
  } = useCommentMyEmotionQuery({
    variables: {where: {id: comment.id}},
  });
  const myEmotion = myEmotionData?.commentMyEmotion?.emotion;

  return (
    <Grid container direction="column" spacing={1}>
      <Grid item>
        <Grid container spacing={2}>
          <Grid item>
            <Avatar></Avatar>
          </Grid>
          <Grid item xs>
            <Grid container direction="column" spacing={1}>
              <Grid item>
                <Grid container spacing={2} alignItems="center">
                  <Grid item>
                    <Typography variant="body2">
                      {comment.user.email}
                    </Typography>
                  </Grid>
                  <Grid item>
                    <Typography variant="caption">
                      {formatDistance(
                        new Date(comment.createdDate),
                        new Date(),
                        {addSuffix: true},
                      )}
                    </Typography>
                  </Grid>
                </Grid>
              </Grid>
              <Grid item>
                <Typography variant="body1">{comment.text}</Typography>
              </Grid>
              <Grid
                container
                justifyContent="space-between"
                alignItems="center">
                <Grid item>
                  <Grid container alignItems="center">
                    <Grid item>
                      <Button
                        variant="text"
                        size="small"
                        startIcon={<ThumbUpOutlined />}
                        onClick={updateEmotion('LIKE')}
                        disabled={
                          myEmotionLoading ||
                          emotionLoading ||
                          undoEmotionLoading
                        }>
                        {comment.like || ''}
                      </Button>
                    </Grid>
                    <Grid item>
                      <Button
                        variant="text"
                        size="small"
                        startIcon={<ThumbDownOutlined />}
                        onClick={updateEmotion('UNLIKE')}
                        disabled={
                          myEmotionLoading ||
                          emotionLoading ||
                          undoEmotionLoading
                        }>
                        {comment.unlike || ''}
                      </Button>
                    </Grid>
                    {post ? (
                      <Grid item>
                        <Button
                          variant="text"
                          size="small"
                          onClick={toggleCommentDrawer(true)}
                          disabled>
                          답글
                        </Button>
                        <Drawer
                          anchor="bottom"
                          open={commentDrawer}
                          onClose={toggleCommentDrawer(false)}>
                          <Container maxWidth="md">
                            <Box pt={2} pb={2}>
                              <CommentCreateFormModule />
                            </Box>
                          </Container>
                        </Drawer>
                      </Grid>
                    ) : (
                      undefined
                    )}
                  </Grid>
                </Grid>
                <Grid item>
                  <IconButton onClick={toggleMoreVert(true)} disabled>
                    <MoreVert fontSize="small" />
                  </IconButton>
                  <Menu
                    anchorEl={moreVert}
                    open={openMoreVert}
                    onClose={toggleMoreVert(false)}>
                    <MenuItem
                      // selected={option === 'Pyxis'}
                      onClick={toggleMoreVert(false)}>
                      <ListItemIcon>
                        <AssistantPhotoOutlined />
                      </ListItemIcon>
                      <Typography variant="inherit">신고</Typography>
                    </MenuItem>
                  </Menu>
                </Grid>
              </Grid>
              {/* {post ? (
                <Grid item>
                  <Grid container direction="column" spacing={2}>
                    <Grid item>
                      <Button
                        variant="text"
                        size="small"
                        startIcon={<KeyboardArrowDown />}>
                        답글 1개 보기
                      </Button>
                    </Grid>
                    <Grid item>
                      <CommentItemModule comment />
                    </Grid>
                  </Grid>
                </Grid>
              ) : (
                undefined
              )} */}
            </Grid>
          </Grid>
        </Grid>
      </Grid>
    </Grid>
  );
}
Example #8
Source File: UserName.tsx    From glide-frontend with GNU General Public License v3.0 4 votes vote down vote up
UserName: React.FC = () => {
  const [isAcknowledged, setIsAcknowledged] = useState(false)
  const { teamId, selectedNft, userName, actions, minimumCakeRequired, allowance } = useProfileCreation()
  const { t } = useTranslation()
  const { account } = useWeb3React()
  const { toastError } = useToast()
  const { library } = useWeb3Provider()
  const [existingUserState, setExistingUserState] = useState<ExistingUserState>(ExistingUserState.IDLE)
  const [isValid, setIsValid] = useState(false)
  const [isLoading, setIsLoading] = useState(false)
  const [message, setMessage] = useState('')
  const hasMinimumCakeRequired = useHasCakeBalance(minimumCakeToRegister)
  const [onPresentConfirmProfileCreation] = useModal(
    <ConfirmProfileCreationModal
      userName={userName}
      selectedNft={selectedNft}
      account={account}
      teamId={teamId}
      minimumCakeRequired={minimumCakeRequired}
      allowance={allowance}
    />,
    false,
  )
  const isUserCreated = existingUserState === ExistingUserState.CREATED

  const checkUsernameValidity = debounce(async (value: string) => {
    try {
      setIsLoading(true)
      const res = await fetch(`${profileApiUrl}/api/users/valid/${value}`)

      if (res.ok) {
        setIsValid(true)
        setMessage('')
      } else {
        const data = await res.json()
        setIsValid(false)
        setMessage(data?.error?.message)
      }
    } finally {
      setIsLoading(false)
    }
  }, 200)

  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    const { value } = event.target
    actions.setUserName(value)
    checkUsernameValidity(value)
  }

  const handleConfirm = async () => {
    try {
      setIsLoading(true)

      const signature = await signMessage(library, account, userName)
      const response = await fetch(`${profileApiUrl}/api/users/register`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          address: account,
          username: userName,
          signature,
        }),
      })

      if (response.ok) {
        setExistingUserState(ExistingUserState.CREATED)
      } else {
        const data = await response.json()
        toastError(t('Error'), data?.error?.message)
      }
    } catch (error) {
      // @ts-ignore
      toastError(error?.message ? error.message : JSON.stringify(error))
    } finally {
      setIsLoading(false)
    }
  }

  const handleAcknowledge = () => setIsAcknowledged(!isAcknowledged)

  // Perform an initial check to see if the wallet has already created a username
  useEffect(() => {
    const fetchUser = async () => {
      try {
        const response = await fetch(`${profileApiUrl}/api/users/${account}`)
        const data = await response.json()

        if (response.ok) {
          const dateCreated = formatDistance(parseISO(data.created_at), new Date())
          setMessage(t('Created %dateCreated% ago', { dateCreated }))

          actions.setUserName(data.username)
          setExistingUserState(ExistingUserState.CREATED)
          setIsValid(true)
        } else {
          setExistingUserState(ExistingUserState.NEW)
        }
      } catch (error) {
        toastError(t('Error'), t('Unable to verify username'))
      }
    }

    if (account) {
      fetchUser()
    }
  }, [account, setExistingUserState, setIsValid, setMessage, actions, toastError, t])

  return (
    <>
      <Text fontSize="20px" color="textSubtle" bold>
        {t('Step %num%', { num: 4 })}
      </Text>
      <Heading as="h3" scale="xl" mb="24px">
        {t('Set Your Name')}
      </Heading>
      <Text as="p" mb="24px">
        {t('This name will be shown in team leaderboards and search results as long as your profile is active.')}
      </Text>
      <Card mb="24px">
        <CardBody>
          <Heading as="h4" scale="lg" mb="8px">
            {t('Set Your Name')}
          </Heading>
          <Text as="p" color="textSubtle" mb="24px">
            {t(
              'Your name must be at least 3 and at most 15 standard letters and numbers long. You can’t change this once you click Confirm.',
            )}
          </Text>
          {existingUserState === ExistingUserState.IDLE ? (
            <Skeleton height="40px" width="240px" />
          ) : (
            <InputWrap>
              <Input
                onChange={handleChange}
                isWarning={userName && !isValid}
                isSuccess={userName && isValid}
                minLength={USERNAME_MIN_LENGTH}
                maxLength={USERNAME_MAX_LENGTH}
                disabled={isUserCreated}
                placeholder={t('Enter your name...')}
                value={userName}
              />
              <Indicator>
                {isLoading && <AutoRenewIcon spin />}
                {!isLoading && isValid && userName && <CheckmarkIcon color="success" />}
                {!isLoading && !isValid && userName && <WarningIcon color="failure" />}
              </Indicator>
            </InputWrap>
          )}
          <Text color="textSubtle" fontSize="14px" py="4px" mb="16px" style={{ minHeight: '30px' }}>
            {message}
          </Text>
          <Text as="p" color="failure" mb="8px">
            {t(
              "Only reuse a name from other social media if you're OK with people viewing your wallet. You can't change your name once you click Confirm.",
            )}
          </Text>
          <label htmlFor="checkbox" style={{ display: 'block', cursor: 'pointer', marginBottom: '24px' }}>
            <Flex alignItems="center">
              <div style={{ flex: 'none' }}>
                <Checkbox id="checkbox" scale="sm" checked={isAcknowledged} onChange={handleAcknowledge} />
              </div>
              <Text ml="8px">{t('I understand that people can view my wallet if they know my username')}</Text>
            </Flex>
          </label>
          <Button onClick={handleConfirm} disabled={!isValid || isUserCreated || isLoading || !isAcknowledged}>
            {t('Confirm')}
          </Button>
        </CardBody>
      </Card>
      <Button
        onClick={onPresentConfirmProfileCreation}
        disabled={!isValid || !isUserCreated}
        id="completeProfileCreation"
      >
        {t('Complete Profile')}
      </Button>
      {!hasMinimumCakeRequired && (
        <Text color="failure" mt="16px">
          {t('A minimum of %num% CAKE is required', { num: REGISTER_COST })}
        </Text>
      )}
    </>
  )
}
Example #9
Source File: index.tsx    From payload with MIT License 4 votes vote down vote up
Autosave: React.FC<Props> = ({ collection, global, id, publishedDocUpdatedAt }) => {
  const { serverURL, routes: { api, admin } } = useConfig();
  const { versions, getVersions } = useDocumentInfo();
  const { fields, dispatchFields } = useWatchForm();
  const modified = useFormModified();
  const locale = useLocale();
  const { replace } = useHistory();

  let interval = 800;
  if (collection?.versions.drafts && collection.versions?.drafts?.autosave) interval = collection.versions.drafts.autosave.interval;
  if (global?.versions.drafts && global.versions?.drafts?.autosave) interval = global.versions.drafts.autosave.interval;

  const [saving, setSaving] = useState(false);
  const [lastSaved, setLastSaved] = useState<number>();
  const debouncedFields = useDebounce(fields, interval);
  const fieldRef = useRef(fields);

  // Store fields in ref so the autosave func
  // can always retrieve the most to date copies
  // after the timeout has executed
  fieldRef.current = fields;

  const createCollectionDoc = useCallback(async () => {
    const res = await fetch(`${serverURL}${api}/${collection.slug}?locale=${locale}&fallback-locale=null&depth=0&draft=true`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({}),
    });

    if (res.status === 201) {
      const json = await res.json();
      replace(`${admin}/collections/${collection.slug}/${json.doc.id}`, {
        state: {
          data: json.doc,
        },
      });
    } else {
      toast.error('There was a problem while autosaving this document.');
    }
  }, [collection, serverURL, api, admin, locale, replace]);

  useEffect(() => {
    // If no ID, but this is used for a collection doc,
    // Immediately save it and set lastSaved
    if (!id && collection) {
      createCollectionDoc();
    }
  }, [id, collection, createCollectionDoc]);

  // When debounced fields change, autosave

  useEffect(() => {
    const autosave = async () => {
      if (modified) {
        setSaving(true);

        let url: string;
        let method: string;

        if (collection && id) {
          url = `${serverURL}${api}/${collection.slug}/${id}?draft=true&autosave=true`;
          method = 'PUT';
        }

        if (global) {
          url = `${serverURL}${api}/globals/${global.slug}?draft=true&autosave=true`;
          method = 'POST';
        }

        if (url) {
          const body = {
            ...reduceFieldsToValues(fieldRef.current),
            _status: 'draft',
          };

          setTimeout(async () => {
            const res = await fetch(url, {
              method,
              headers: {
                'Content-Type': 'application/json',
              },
              body: JSON.stringify(body),
            });

            setSaving(false);

            if (res.status === 200) {
              setLastSaved(new Date().getTime());
              getVersions();
            }
          }, 1000);
        }
      }
    };

    autosave();
  }, [debouncedFields, modified, serverURL, api, collection, global, id, dispatchFields, getVersions]);

  useEffect(() => {
    if (versions?.docs?.[0]) {
      setLastSaved(new Date(versions.docs[0].updatedAt).getTime());
    } else if (publishedDocUpdatedAt) {
      setLastSaved(new Date(publishedDocUpdatedAt).getTime());
    }
  }, [publishedDocUpdatedAt, versions]);

  return (
    <div className={baseClass}>
      {saving && 'Saving...'}
      {(!saving && lastSaved) && (
        <React.Fragment>
          Last saved&nbsp;
          {formatDistance(new Date(), new Date(lastSaved))}
          &nbsp;ago
        </React.Fragment>
      )}
    </div>
  );
}
Example #10
Source File: UserName.tsx    From vvs-ui with GNU General Public License v3.0 4 votes vote down vote up
UserName: React.FC = () => {
  const [isAcknowledged, setIsAcknowledged] = useState(false)
  const { teamId, selectedNft, userName, actions, minimumCakeRequired, allowance } = useProfileCreation()
  const { t } = useTranslation()
  const { account } = useWeb3React()
  const { toastError } = useToast()
  const { library } = useWeb3Provider()
  const [existingUserState, setExistingUserState] = useState<ExistingUserState>(ExistingUserState.IDLE)
  const [isValid, setIsValid] = useState(false)
  const [isLoading, setIsLoading] = useState(false)
  const [message, setMessage] = useState('')
  const { balance: cakeBalance, fetchStatus } = useGetVvsBalance()
  const hasMinimumCakeRequired = fetchStatus === FetchStatus.SUCCESS && cakeBalance.gte(REGISTER_COST)
  const [onPresentConfirmProfileCreation] = useModal(
    <ConfirmProfileCreationModal
      userName={userName}
      selectedNft={selectedNft}
      account={account}
      teamId={teamId}
      minimumCakeRequired={minimumCakeRequired}
      allowance={allowance}
    />,
    false,
  )
  const isUserCreated = existingUserState === ExistingUserState.CREATED

  const checkUsernameValidity = debounce(async (value: string) => {
    try {
      setIsLoading(true)
      const res = await fetch(`${API_PROFILE}/api/users/valid/${value}`)

      if (res.ok) {
        setIsValid(true)
        setMessage('')
      } else {
        const data = await res.json()
        setIsValid(false)
        setMessage(data?.error?.message)
      }
    } finally {
      setIsLoading(false)
    }
  }, 200)

  const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
    const { value } = event.target
    actions.setUserName(value)
    checkUsernameValidity(value)
  }

  const handleConfirm = async () => {
    try {
      setIsLoading(true)

      const signature = await signMessage(library, account, userName)
      const response = await fetch(`${API_PROFILE}/api/users/register`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({
          address: account,
          username: userName,
          signature,
        }),
      })

      if (response.ok) {
        setExistingUserState(ExistingUserState.CREATED)
      } else {
        const data = await response.json()
        toastError(t('Error'), data?.error?.message)
      }
    } catch (error) {
      toastError(error?.message ? error.message : JSON.stringify(error))
    } finally {
      setIsLoading(false)
    }
  }

  const handleAcknowledge = () => setIsAcknowledged(!isAcknowledged)

  // Perform an initial check to see if the wallet has already created a username
  useEffect(() => {
    const fetchUser = async () => {
      try {
        const response = await fetch(`${API_PROFILE}/api/users/${account}`)
        const data = await response.json()

        if (response.ok) {
          const dateCreated = formatDistance(parseISO(data.created_at), new Date())
          setMessage(t('Created %dateCreated% ago', { dateCreated }))

          actions.setUserName(data.username)
          setExistingUserState(ExistingUserState.CREATED)
          setIsValid(true)
        } else {
          setExistingUserState(ExistingUserState.NEW)
        }
      } catch (error) {
        toastError(t('Error'), t('Unable to verify username'))
      }
    }

    if (account) {
      fetchUser()
    }
  }, [account, setExistingUserState, setIsValid, setMessage, actions, toastError, t])

  return (
    <>
      <Text fontSize="20px" color="textSubtle" bold>
        {t('Step %num%', { num: 4 })}
      </Text>
      <Heading as="h3" scale="xl" mb="24px">
        {t('Set Your Name')}
      </Heading>
      <Text as="p" mb="24px">
        {t('This name will be shown in team leaderboards and search results as long as your profile is active.')}
      </Text>
      <Card mb="24px">
        <CardBody>
          <Heading as="h4" scale="lg" mb="8px">
            {t('Set Your Name')}
          </Heading>
          <Text as="p" color="textSubtle" mb="24px">
            {t(
              'Your name must be at least 3 and at most 15 standard letters and numbers long. You can’t change this once you click Confirm.',
            )}
          </Text>
          {existingUserState === ExistingUserState.IDLE ? (
            <Skeleton height="40px" width="240px" />
          ) : (
            <InputWrap>
              <Input
                onChange={handleChange}
                isWarning={userName && !isValid}
                isSuccess={userName && isValid}
                minLength={USERNAME_MIN_LENGTH}
                maxLength={USERNAME_MAX_LENGTH}
                disabled={isUserCreated}
                placeholder={t('Enter your name...')}
                value={userName}
              />
              <Indicator>
                {isLoading && <AutoRenewIcon spin />}
                {!isLoading && isValid && userName && <CheckmarkIcon color="success" />}
                {!isLoading && !isValid && userName && <WarningIcon color="failure" />}
              </Indicator>
            </InputWrap>
          )}
          <Text color="textSubtle" fontSize="14px" py="4px" mb="16px" style={{ minHeight: '30px' }}>
            {message}
          </Text>
          <Text as="p" color="failure" mb="8px">
            {t(
              "Only reuse a name from other social media if you're OK with people viewing your wallet. You can't change your name once you click Confirm.",
            )}
          </Text>
          <label htmlFor="checkbox" style={{ display: 'block', cursor: 'pointer', marginBottom: '24px' }}>
            <Flex alignItems="center">
              <div style={{ flex: 'none' }}>
                <Checkbox id="checkbox" scale="sm" checked={isAcknowledged} onChange={handleAcknowledge} />
              </div>
              <Text ml="8px">{t('I understand that people can view my wallet if they know my username')}</Text>
            </Flex>
          </label>
          <Button onClick={handleConfirm} disabled={!isValid || isUserCreated || isLoading || !isAcknowledged}>
            {t('Confirm')}
          </Button>
        </CardBody>
      </Card>
      <Button
        onClick={onPresentConfirmProfileCreation}
        disabled={!isValid || !isUserCreated}
        id="completeProfileCreation"
      >
        {t('Complete Profile')}
      </Button>
      {!hasMinimumCakeRequired && (
        <Text color="failure" mt="16px">
          {t('A minimum of %num% VVS is required', { num: formatUnits(REGISTER_COST) })}
        </Text>
      )}
    </>
  )
}