From f79554371d86db3eed99318b4ac9da6c27f9b512 Mon Sep 17 00:00:00 2001 From: Steven Date: Mon, 13 May 2024 07:57:58 +0800 Subject: [PATCH] chore: retire share dialog --- web/src/components/MemoActionMenu.tsx | 15 +- web/src/components/ShareMemoDialog.tsx | 184 ------------------ .../html2image/convertResourceToDataURL.ts | 22 --- .../labs/html2image/getCloneStyledElement.ts | 40 ---- web/src/labs/html2image/index.ts | 106 ---------- web/src/labs/html2image/waitImageLoaded.ts | 17 -- web/src/less/share-memo-dialog.less | 5 - 7 files changed, 10 insertions(+), 379 deletions(-) delete mode 100644 web/src/components/ShareMemoDialog.tsx delete mode 100644 web/src/labs/html2image/convertResourceToDataURL.ts delete mode 100644 web/src/labs/html2image/getCloneStyledElement.ts delete mode 100644 web/src/labs/html2image/index.ts delete mode 100644 web/src/labs/html2image/waitImageLoaded.ts delete mode 100644 web/src/less/share-memo-dialog.less diff --git a/web/src/components/MemoActionMenu.tsx b/web/src/components/MemoActionMenu.tsx index 0bbac02e7..d09b02754 100644 --- a/web/src/components/MemoActionMenu.tsx +++ b/web/src/components/MemoActionMenu.tsx @@ -1,16 +1,16 @@ import { Dropdown, Menu, MenuButton, MenuItem } from "@mui/joy"; import clsx from "clsx"; +import copy from "copy-to-clipboard"; import toast from "react-hot-toast"; import { useLocation } from "react-router-dom"; import Icon from "@/components/Icon"; import useNavigateTo from "@/hooks/useNavigateTo"; -import { extractMemoIdFromName, useMemoStore } from "@/store/v1"; +import { useMemoStore } from "@/store/v1"; import { RowStatus } from "@/types/proto/api/v1/common"; import { Memo } from "@/types/proto/api/v1/memo_service"; import { useTranslate } from "@/utils/i18n"; import { showCommonDialog } from "./Dialog/CommonDialog"; import showMemoEditorDialog from "./MemoEditor/MemoEditorDialog"; -import showShareMemoDialog from "./ShareMemoDialog"; interface Props { memo: Memo; @@ -89,6 +89,11 @@ const MemoActionMenu = (props: Props) => { } }; + const handleCopyLink = () => { + copy(`${window.location.origin}/m/${memo.uid}`); + toast.success(t("message.succeed-copy-link")); + }; + const handleDeleteMemoClick = async () => { showCommonDialog({ title: t("memo.delete-memo"), @@ -126,9 +131,9 @@ const MemoActionMenu = (props: Props) => { )} {!hiddenActions?.includes("share") && ( - showShareMemoDialog(extractMemoIdFromName(memo.name))}> - - {t("common.share")} + + + {t("memo.copy-link")} )} diff --git a/web/src/components/ShareMemoDialog.tsx b/web/src/components/ShareMemoDialog.tsx deleted file mode 100644 index 19060b9fc..000000000 --- a/web/src/components/ShareMemoDialog.tsx +++ /dev/null @@ -1,184 +0,0 @@ -import { Button, IconButton, Select, Option } from "@mui/joy"; -import copy from "copy-to-clipboard"; -import React, { useEffect, useRef } from "react"; -import { toast } from "react-hot-toast"; -import { getDateTimeString } from "@/helpers/datetime"; -import { downloadFileFromUrl } from "@/helpers/utils"; -import useCurrentUser from "@/hooks/useCurrentUser"; -import useLoading from "@/hooks/useLoading"; -import toImage from "@/labs/html2image"; -import { useUserStore, useMemoStore, MemoNamePrefix } from "@/store/v1"; -import { Visibility } from "@/types/proto/api/v1/memo_service"; -import { useTranslate } from "@/utils/i18n"; -import { convertVisibilityToString } from "@/utils/memo"; -import { generateDialog } from "./Dialog"; -import Icon from "./Icon"; -import MemoContent from "./MemoContent"; -import MemoResourceListView from "./MemoResourceListView"; -import UserAvatar from "./UserAvatar"; -import VisibilityIcon from "./VisibilityIcon"; -import "@/less/share-memo-dialog.less"; - -interface Props extends DialogProps { - memoId: number; -} - -const ShareMemoDialog: React.FC = (props: Props) => { - const { memoId, destroy } = props; - const t = useTranslate(); - const currentUser = useCurrentUser(); - const userStore = useUserStore(); - const memoStore = useMemoStore(); - const downloadingImageState = useLoading(false); - const loadingState = useLoading(); - const memoContainerRef = useRef(null); - const memo = memoStore.getMemoByName(`${MemoNamePrefix}${memoId}`); - const user = userStore.getUserByName(memo.creator); - const readonly = memo?.creator !== currentUser?.name; - - useEffect(() => { - (async () => { - await userStore.getOrFetchUserByName(memo.creator); - loadingState.setFinish(); - })(); - }, []); - - const handleCloseBtnClick = () => { - destroy(); - }; - - const handleDownloadImageBtnClick = () => { - if (!memoContainerRef.current) { - return; - } - - downloadingImageState.setLoading(); - toImage(memoContainerRef.current, { - pixelRatio: window.devicePixelRatio * 2, - }) - .then((url) => { - downloadFileFromUrl(url, `memos-${getDateTimeString(Date.now())}.png`); - downloadingImageState.setFinish(); - URL.revokeObjectURL(url); - }) - .catch((err) => { - console.error(err); - }); - }; - - const handleDownloadTextFileBtnClick = () => { - const blob = new Blob([memo.content], { type: "text/plain;charset=utf-8" }); - const url = URL.createObjectURL(blob); - downloadFileFromUrl(url, `memos-${getDateTimeString(Date.now())}.md`); - URL.revokeObjectURL(url); - }; - - const handleCopyLinkBtnClick = () => { - copy(`${window.location.origin}/m/${memo.uid}`); - toast.success(t("message.succeed-copy-link")); - }; - - const handleMemoVisibilityOptionChanged = async (visibility: Visibility) => { - const updatedMemo = await memoStore.updateMemo( - { - name: memo.name, - visibility: visibility, - }, - ["visibility"], - ); - - if (updatedMemo.visibility == visibility) { - toast.success(t("message.update-succeed")); - } - }; - - if (loadingState.isLoading) { - return null; - } - - return ( - <> -
-

{t("common.share")} Memo

- - - -
-
-
-
- - - -
- {!readonly && ( - - )} -
-
-
- {getDateTimeString(memo.displayTime)} -
- - -
-
-
- -
- - {user.nickname || user.username} - -
-
- via memos -
-
-
-
- - ); -}; - -export default function showShareMemoDialog(memoId: number): void { - generateDialog( - { - className: "share-memo-dialog", - dialogName: "share-memo-dialog", - }, - ShareMemoDialog, - { memoId }, - ); -} diff --git a/web/src/labs/html2image/convertResourceToDataURL.ts b/web/src/labs/html2image/convertResourceToDataURL.ts deleted file mode 100644 index 0c8c71418..000000000 --- a/web/src/labs/html2image/convertResourceToDataURL.ts +++ /dev/null @@ -1,22 +0,0 @@ -const cachedResourceMap = new Map(); - -const convertResourceToDataURL = async (url: string, useCache = true): Promise => { - if (useCache && cachedResourceMap.has(url)) { - return Promise.resolve(cachedResourceMap.get(url) as string); - } - - const res = await fetch(url); - const blob = await res.blob(); - - return new Promise((resolve) => { - const reader = new FileReader(); - reader.onloadend = () => { - const base64Url = reader.result as string; - cachedResourceMap.set(url, base64Url); - resolve(base64Url); - }; - reader.readAsDataURL(blob); - }); -}; - -export default convertResourceToDataURL; diff --git a/web/src/labs/html2image/getCloneStyledElement.ts b/web/src/labs/html2image/getCloneStyledElement.ts deleted file mode 100644 index 988812d07..000000000 --- a/web/src/labs/html2image/getCloneStyledElement.ts +++ /dev/null @@ -1,40 +0,0 @@ -import convertResourceToDataURL from "./convertResourceToDataURL"; - -const applyStyles = async (sourceElement: HTMLElement, clonedElement: HTMLElement) => { - if (!sourceElement || !clonedElement) { - return; - } - - if (sourceElement.tagName === "IMG") { - const url = sourceElement.getAttribute("src") ?? ""; - let covertFailed = false; - try { - (clonedElement as HTMLImageElement).src = await convertResourceToDataURL(url); - } catch (error) { - covertFailed = true; - } - if (covertFailed) { - throw new Error(`Failed to convert image to data URL: ${url}`); - } - } - - const sourceStyles = window.getComputedStyle(sourceElement); - for (const item of sourceStyles) { - clonedElement.style.setProperty(item, sourceStyles.getPropertyValue(item), sourceStyles.getPropertyPriority(item)); - } - - for (let i = 0; i < clonedElement.childElementCount; i++) { - await applyStyles(sourceElement.children[i] as HTMLElement, clonedElement.children[i] as HTMLElement); - } -}; - -const getCloneStyledElement = async (element: HTMLElement) => { - const clonedElementContainer = document.createElement(element.tagName); - clonedElementContainer.innerHTML = element.innerHTML; - - await applyStyles(element, clonedElementContainer); - - return clonedElementContainer; -}; - -export default getCloneStyledElement; diff --git a/web/src/labs/html2image/index.ts b/web/src/labs/html2image/index.ts deleted file mode 100644 index 4ec677020..000000000 --- a/web/src/labs/html2image/index.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * HTML to Image - * - * References: - * 1. html-to-image: https://github.com/bubkoo/html-to-image - * 2. : https://developer.mozilla.org/en-US/docs/Web/SVG/Element/foreignObject - */ -import getCloneStyledElement from "./getCloneStyledElement"; -import waitImageLoaded from "./waitImageLoaded"; - -type Options = Partial<{ - backgroundColor: string; - pixelRatio: number; -}>; - -const getElementSize = (element: HTMLElement) => { - const { width, height } = window.getComputedStyle(element); - - return { - width: parseInt(width.replace("px", "")), - height: parseInt(height.replace("px", "")), - }; -}; - -const convertSVGToDataURL = (svg: SVGElement): string => { - const xml = new XMLSerializer().serializeToString(svg); - const url = encodeURIComponent(xml); - return `data:image/svg+xml;charset=utf-8,${url}`; -}; - -const generateSVGElement = (width: number, height: number, element: HTMLElement): SVGSVGElement => { - const xmlNS = "http://www.w3.org/2000/svg"; - const svgElement = document.createElementNS(xmlNS, "svg"); - - svgElement.setAttribute("width", `${width}`); - svgElement.setAttribute("height", `${height}`); - svgElement.setAttribute("viewBox", `0 0 ${width} ${height}`); - - const foreignObject = document.createElementNS(xmlNS, "foreignObject"); - - foreignObject.setAttribute("width", "100%"); - foreignObject.setAttribute("height", "100%"); - foreignObject.setAttribute("x", "0"); - foreignObject.setAttribute("y", "0"); - foreignObject.setAttribute("externalResourcesRequired", "true"); - - foreignObject.appendChild(element); - svgElement.appendChild(foreignObject); - - return svgElement; -}; - -export const toSVG = async (element: HTMLElement, options?: Options) => { - const { width, height } = getElementSize(element); - const clonedElement = await getCloneStyledElement(element); - - if (options?.backgroundColor) { - clonedElement.style.backgroundColor = options.backgroundColor; - } - - const svg = generateSVGElement(width, height, clonedElement); - const url = convertSVGToDataURL(svg); - - return url; -}; - -export const toCanvas = async (element: HTMLElement, options?: Options): Promise => { - const ratio = options?.pixelRatio || 1; - const { width, height } = getElementSize(element); - - const canvas = document.createElement("canvas"); - const context = canvas.getContext("2d"); - - if (!context) { - return Promise.reject("Canvas error"); - } - - canvas.width = width * ratio; - canvas.height = height * ratio; - - canvas.style.width = `${width}`; - canvas.style.height = `${height}`; - - if (options?.backgroundColor) { - context.fillStyle = options.backgroundColor; - context.fillRect(0, 0, canvas.width, canvas.height); - } - - const url = await toSVG(element, options); - const imageEl = new Image(); - imageEl.style.zIndex = "-1"; - imageEl.style.position = "fixed"; - imageEl.style.top = "0"; - document.body.append(imageEl); - await waitImageLoaded(imageEl, url); - context.drawImage(imageEl, 0, 0, canvas.width, canvas.height); - imageEl.remove(); - return canvas; -}; - -const toImage = async (element: HTMLElement, options?: Options) => { - const canvas = await toCanvas(element, options); - return canvas.toDataURL(); -}; - -export default toImage; diff --git a/web/src/labs/html2image/waitImageLoaded.ts b/web/src/labs/html2image/waitImageLoaded.ts deleted file mode 100644 index 21263381b..000000000 --- a/web/src/labs/html2image/waitImageLoaded.ts +++ /dev/null @@ -1,17 +0,0 @@ -const waitImageLoaded = (image: HTMLImageElement, url: string): Promise => { - return new Promise((resolve, reject) => { - image.loading = "eager"; - image.onload = () => { - // NOTE: There is image loading problem in Safari, fix it with some trick - setTimeout(() => { - resolve(); - }, 200); - }; - image.onerror = () => { - reject("Image load failed"); - }; - image.src = url; - }); -}; - -export default waitImageLoaded; diff --git a/web/src/less/share-memo-dialog.less b/web/src/less/share-memo-dialog.less deleted file mode 100644 index 48fc1601c..000000000 --- a/web/src/less/share-memo-dialog.less +++ /dev/null @@ -1,5 +0,0 @@ -.share-memo-dialog { - > .dialog-container { - @apply w-112 max-w-full p-0 bg-white dark:bg-zinc-800; - } -}