mirror of
				https://github.com/usememos/memos.git
				synced 2025-06-05 22:09:59 +02:00 
			
		
		
		
	feat: fold memo when content overflow (#1327)
* feat: fold memo when content overflow * chore: update
This commit is contained in:
		| @@ -1,5 +1,5 @@ | |||||||
| import * as utils from "../helpers/utils"; | import * as utils from "../helpers/utils"; | ||||||
| import MemoContent, { DisplayConfig } from "./MemoContent"; | import MemoContent from "./MemoContent"; | ||||||
| import MemoResources from "./MemoResources"; | import MemoResources from "./MemoResources"; | ||||||
| import "../less/daily-memo.less"; | import "../less/daily-memo.less"; | ||||||
|  |  | ||||||
| @@ -10,9 +10,6 @@ interface Props { | |||||||
| const DailyMemo: React.FC<Props> = (props: Props) => { | const DailyMemo: React.FC<Props> = (props: Props) => { | ||||||
|   const { memo } = props; |   const { memo } = props; | ||||||
|   const createdTimeStr = utils.getTimeString(memo.createdTs); |   const createdTimeStr = utils.getTimeString(memo.createdTs); | ||||||
|   const displayConfig: DisplayConfig = { |  | ||||||
|     enableExpand: false, |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   return ( |   return ( | ||||||
|     <div className="daily-memo-wrapper"> |     <div className="daily-memo-wrapper"> | ||||||
| @@ -20,7 +17,7 @@ const DailyMemo: React.FC<Props> = (props: Props) => { | |||||||
|         <span className="normal-text">{createdTimeStr}</span> |         <span className="normal-text">{createdTimeStr}</span> | ||||||
|       </div> |       </div> | ||||||
|       <div className="memo-container"> |       <div className="memo-container"> | ||||||
|         <MemoContent content={memo.content} displayConfig={displayConfig} /> |         <MemoContent content={memo.content} showFull={true} /> | ||||||
|         <MemoResources resourceList={memo.resourceList} /> |         <MemoResources resourceList={memo.resourceList} /> | ||||||
|       </div> |       </div> | ||||||
|       <div className="split-line"></div> |       <div className="split-line"></div> | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| import { Tooltip } from "@mui/joy"; |  | ||||||
| import copy from "copy-to-clipboard"; | import copy from "copy-to-clipboard"; | ||||||
| import dayjs from "dayjs"; | import dayjs from "dayjs"; | ||||||
| import { memo, useEffect, useRef, useState } from "react"; | import { memo, useEffect, useRef, useState } from "react"; | ||||||
| @@ -39,7 +38,6 @@ const Memo: React.FC<Props> = (props: Props) => { | |||||||
|   const [createdTimeStr, setCreatedTimeStr] = useState<string>(getFormatedMemoTimeStr(memo.createdTs, i18n.language)); |   const [createdTimeStr, setCreatedTimeStr] = useState<string>(getFormatedMemoTimeStr(memo.createdTs, i18n.language)); | ||||||
|   const memoContainerRef = useRef<HTMLDivElement>(null); |   const memoContainerRef = useRef<HTMLDivElement>(null); | ||||||
|   const isVisitorMode = userStore.isVisitorMode() || readonly; |   const isVisitorMode = userStore.isVisitorMode() || readonly; | ||||||
|   const updatedTimeStr = getFormatedMemoTimeStr(memo.updatedTs, i18n.language); |  | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     let intervalFlag: any = -1; |     let intervalFlag: any = -1; | ||||||
| @@ -111,7 +109,7 @@ const Memo: React.FC<Props> = (props: Props) => { | |||||||
|     } |     } | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const handleGenMemoImageBtnClick = () => { |   const handleGenerateMemoImageBtnClick = () => { | ||||||
|     showShareMemo(memo); |     showShareMemo(memo); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
| @@ -203,11 +201,9 @@ const Memo: React.FC<Props> = (props: Props) => { | |||||||
|       {memo.pinned && <div className="corner-container"></div>} |       {memo.pinned && <div className="corner-container"></div>} | ||||||
|       <div className="memo-top-wrapper"> |       <div className="memo-top-wrapper"> | ||||||
|         <div className="status-text-container"> |         <div className="status-text-container"> | ||||||
|           <Tooltip title={`Updated at ${updatedTimeStr}`} placement="top" arrow> |  | ||||||
|           <span className="time-text" onDoubleClick={handleMemoCreatedTimeClick}> |           <span className="time-text" onDoubleClick={handleMemoCreatedTimeClick}> | ||||||
|             {createdTimeStr} |             {createdTimeStr} | ||||||
|           </span> |           </span> | ||||||
|           </Tooltip> |  | ||||||
|           {isVisitorMode && ( |           {isVisitorMode && ( | ||||||
|             <a className="name-text" href={`/u/${memo.creatorId}`}> |             <a className="name-text" href={`/u/${memo.creatorId}`}> | ||||||
|               @{memo.creatorName} |               @{memo.creatorName} | ||||||
| @@ -238,7 +234,7 @@ const Memo: React.FC<Props> = (props: Props) => { | |||||||
|                     <Icon.Edit3 className="icon-img" /> |                     <Icon.Edit3 className="icon-img" /> | ||||||
|                     <span className="tip-text">{t("common.edit")}</span> |                     <span className="tip-text">{t("common.edit")}</span> | ||||||
|                   </div> |                   </div> | ||||||
|                   <div className="btn" onClick={handleGenMemoImageBtnClick}> |                   <div className="btn" onClick={handleGenerateMemoImageBtnClick}> | ||||||
|                     <Icon.Share className="icon-img" /> |                     <Icon.Share className="icon-img" /> | ||||||
|                     <span className="tip-text">{t("common.share")}</span> |                     <span className="tip-text">{t("common.share")}</span> | ||||||
|                   </div> |                   </div> | ||||||
|   | |||||||
| @@ -1,18 +1,15 @@ | |||||||
| import { useEffect, useMemo, useRef, useState } from "react"; | import { useEffect, useRef, useState } from "react"; | ||||||
| import { useTranslation } from "react-i18next"; | import { useTranslation } from "react-i18next"; | ||||||
| import { useUserStore } from "../store/module"; |  | ||||||
| import { marked } from "../labs/marked"; | import { marked } from "../labs/marked"; | ||||||
| import Icon from "./Icon"; | import Icon from "./Icon"; | ||||||
| import "../less/memo-content.less"; | import "../less/memo-content.less"; | ||||||
|  |  | ||||||
| export interface DisplayConfig { | const MAX_EXPAND_HEIGHT = 384; | ||||||
|   enableExpand: boolean; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| interface Props { | interface Props { | ||||||
|   content: string; |   content: string; | ||||||
|   className?: string; |   className?: string; | ||||||
|   displayConfig?: Partial<DisplayConfig>; |   showFull?: boolean; | ||||||
|   onMemoContentClick?: (e: React.MouseEvent) => void; |   onMemoContentClick?: (e: React.MouseEvent) => void; | ||||||
|   onMemoContentDoubleClick?: (e: React.MouseEvent) => void; |   onMemoContentDoubleClick?: (e: React.MouseEvent) => void; | ||||||
| } | } | ||||||
| @@ -23,48 +20,29 @@ interface State { | |||||||
|   expandButtonStatus: ExpandButtonStatus; |   expandButtonStatus: ExpandButtonStatus; | ||||||
| } | } | ||||||
|  |  | ||||||
| const defaultDisplayConfig: DisplayConfig = { |  | ||||||
|   enableExpand: true, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| const MemoContent: React.FC<Props> = (props: Props) => { | const MemoContent: React.FC<Props> = (props: Props) => { | ||||||
|   const { className, content, onMemoContentClick, onMemoContentDoubleClick } = props; |   const { className, content, showFull, onMemoContentClick, onMemoContentDoubleClick } = props; | ||||||
|   const { t } = useTranslation(); |   const { t } = useTranslation(); | ||||||
|   const userStore = useUserStore(); |  | ||||||
|   const user = userStore.state.user; |  | ||||||
|   const foldedContent = useMemo(() => { |  | ||||||
|     const firstHorizontalRuleIndex = content.search(/^---$|^\*\*\*$|^___$/m); |  | ||||||
|     return firstHorizontalRuleIndex !== -1 ? content.slice(0, firstHorizontalRuleIndex) : content; |  | ||||||
|   }, [content]); |  | ||||||
|  |  | ||||||
|   const [state, setState] = useState<State>({ |   const [state, setState] = useState<State>({ | ||||||
|     expandButtonStatus: -1, |     expandButtonStatus: -1, | ||||||
|   }); |   }); | ||||||
|   const memoContentContainerRef = useRef<HTMLDivElement>(null); |   const memoContentContainerRef = useRef<HTMLDivElement>(null); | ||||||
|   const displayConfig = { |  | ||||||
|     ...defaultDisplayConfig, |  | ||||||
|     ...props.displayConfig, |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     if (!memoContentContainerRef) { |     if (showFull) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (displayConfig.enableExpand && user && user.localSetting.enableFoldMemo) { |     if (memoContentContainerRef.current) { | ||||||
|       if (foldedContent.length !== content.length) { |       const height = memoContentContainerRef.current.clientHeight; | ||||||
|  |       if (height > MAX_EXPAND_HEIGHT) { | ||||||
|         setState({ |         setState({ | ||||||
|           ...state, |  | ||||||
|           expandButtonStatus: 0, |           expandButtonStatus: 0, | ||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|     } else { |  | ||||||
|       setState({ |  | ||||||
|         ...state, |  | ||||||
|         expandButtonStatus: -1, |  | ||||||
|       }); |  | ||||||
|     } |     } | ||||||
|   }, [user?.localSetting.enableFoldMemo, content]); |   }, []); | ||||||
|  |  | ||||||
|   const handleMemoContentClick = async (e: React.MouseEvent) => { |   const handleMemoContentClick = async (e: React.MouseEvent) => { | ||||||
|     if (onMemoContentClick) { |     if (onMemoContentClick) { | ||||||
| @@ -89,17 +67,18 @@ const MemoContent: React.FC<Props> = (props: Props) => { | |||||||
|     <div className={`memo-content-wrapper ${className || ""}`}> |     <div className={`memo-content-wrapper ${className || ""}`}> | ||||||
|       <div |       <div | ||||||
|         ref={memoContentContainerRef} |         ref={memoContentContainerRef} | ||||||
|         className={`memo-content-text ${state.expandButtonStatus === 0 ? "expanded" : ""}`} |         className={`memo-content-text ${state.expandButtonStatus === 0 ? "max-h-64 overflow-y-hidden" : ""}`} | ||||||
|         onClick={handleMemoContentClick} |         onClick={handleMemoContentClick} | ||||||
|         onDoubleClick={handleMemoContentDoubleClick} |         onDoubleClick={handleMemoContentDoubleClick} | ||||||
|       > |       > | ||||||
|         {marked(state.expandButtonStatus === 0 ? foldedContent : content)} |         {marked(content)} | ||||||
|       </div> |       </div> | ||||||
|       {state.expandButtonStatus !== -1 && ( |       {state.expandButtonStatus !== -1 && ( | ||||||
|         <div className="expand-btn-container"> |         <div className={`expand-btn-container ${state.expandButtonStatus === 0 && "!-mt-7"}`}> | ||||||
|           <span className={`btn ${state.expandButtonStatus === 0 ? "expand-btn" : "fold-btn"}`} onClick={handleExpandBtnClick}> |           <div className="absolute top-0 left-0 w-full h-full blur-lg bg-white"></div> | ||||||
|  |           <span className={`btn z-10 ${state.expandButtonStatus === 0 ? "expand-btn" : "fold-btn"}`} onClick={handleExpandBtnClick}> | ||||||
|             {state.expandButtonStatus === 0 ? t("common.expand") : t("common.fold")} |             {state.expandButtonStatus === 0 ? t("common.expand") : t("common.fold")} | ||||||
|             <Icon.ChevronRight className="icon-img" /> |             <Icon.ChevronRight className="icon-img opacity-80" /> | ||||||
|           </span> |           </span> | ||||||
|         </div> |         </div> | ||||||
|       )} |       )} | ||||||
|   | |||||||
| @@ -40,10 +40,6 @@ const PreferencesSection = () => { | |||||||
|     await userStore.upsertUserSetting("resourceVisibility", value); |     await userStore.upsertUserSetting("resourceVisibility", value); | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   const handleIsFoldingEnabledChanged = (event: React.ChangeEvent<HTMLInputElement>) => { |  | ||||||
|     userStore.upsertLocalSetting({ ...localSetting, enableFoldMemo: event.target.checked }); |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   const handleDoubleClickEnabledChanged = (event: React.ChangeEvent<HTMLInputElement>) => { |   const handleDoubleClickEnabledChanged = (event: React.ChangeEvent<HTMLInputElement>) => { | ||||||
|     userStore.upsertLocalSetting({ ...localSetting, enableDoubleClickEditing: event.target.checked }); |     userStore.upsertLocalSetting({ ...localSetting, enableDoubleClickEditing: event.target.checked }); | ||||||
|   }; |   }; | ||||||
| @@ -131,10 +127,6 @@ const PreferencesSection = () => { | |||||||
|         </span> |         </span> | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|       <label className="form-label selector"> |  | ||||||
|         <span className="normal-text">{t("setting.preference-section.enable-folding-memo")}</span> |  | ||||||
|         <Switch className="ml-2" checked={localSetting.enableFoldMemo} onChange={handleIsFoldingEnabledChanged} /> |  | ||||||
|       </label> |  | ||||||
|       <label className="form-label selector"> |       <label className="form-label selector"> | ||||||
|         <span className="normal-text">{t("setting.preference-section.enable-double-click")}</span> |         <span className="normal-text">{t("setting.preference-section.enable-double-click")}</span> | ||||||
|         <Switch className="ml-2" checked={localSetting.enableDoubleClickEditing} onChange={handleDoubleClickEnabledChanged} /> |         <Switch className="ml-2" checked={localSetting.enableDoubleClickEditing} onChange={handleDoubleClickEnabledChanged} /> | ||||||
|   | |||||||
| @@ -126,7 +126,7 @@ const ShareMemoDialog: React.FC<Props> = (props: Props) => { | |||||||
|         <div className="memo-container" ref={memoElRef}> |         <div className="memo-container" ref={memoElRef}> | ||||||
|           <span className="time-text">{memo.createdAtStr}</span> |           <span className="time-text">{memo.createdAtStr}</span> | ||||||
|           <div className="memo-content-wrapper"> |           <div className="memo-content-wrapper"> | ||||||
|             <MemoContent content={memo.content} displayConfig={{ enableExpand: false }} /> |             <MemoContent content={memo.content} showFull={true} /> | ||||||
|             <MemoResources resourceList={memo.resourceList} /> |             <MemoResources resourceList={memo.resourceList} /> | ||||||
|           </div> |           </div> | ||||||
|           <div className="watermark-container"> |           <div className="watermark-container"> | ||||||
|   | |||||||
| @@ -110,11 +110,9 @@ | |||||||
|     @apply w-full relative flex flex-row justify-start items-center; |     @apply w-full relative flex flex-row justify-start items-center; | ||||||
|  |  | ||||||
|     > .btn { |     > .btn { | ||||||
|       @apply flex flex-row justify-start items-center pl-2 pr-1 py-1 my-1 text-xs rounded-lg border bg-gray-100 dark:bg-zinc-600 border-gray-200 dark:border-zinc-600 opacity-80 shadow hover:opacity-60 cursor-pointer; |       @apply flex flex-row justify-start items-center pl-2 pr-1 py-1 my-2 text-xs rounded-lg border bg-gray-100 dark:bg-zinc-600 border-gray-200 dark:border-zinc-600 shadow hover:opacity-90 cursor-pointer; | ||||||
|  |  | ||||||
|       &.expand-btn { |       &.expand-btn { | ||||||
|         @apply mt-2; |  | ||||||
|  |  | ||||||
|         > .icon-img { |         > .icon-img { | ||||||
|           @apply rotate-90; |           @apply rotate-90; | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -14,7 +14,6 @@ const defaultSetting: Setting = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| const defaultLocalSetting: LocalSetting = { | const defaultLocalSetting: LocalSetting = { | ||||||
|   enableFoldMemo: true, |  | ||||||
|   enableDoubleClickEditing: true, |   enableDoubleClickEditing: true, | ||||||
|   dailyReviewTimeOffset: 0, |   dailyReviewTimeOffset: 0, | ||||||
| }; | }; | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								web/src/types/modules/setting.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								web/src/types/modules/setting.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -8,7 +8,6 @@ interface Setting { | |||||||
| } | } | ||||||
|  |  | ||||||
| interface LocalSetting { | interface LocalSetting { | ||||||
|   enableFoldMemo: boolean; |  | ||||||
|   enableDoubleClickEditing: boolean; |   enableDoubleClickEditing: boolean; | ||||||
|   dailyReviewTimeOffset: number; |   dailyReviewTimeOffset: number; | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user