From b04e628e003585b5b0956c4b1acd859d3e8186dc Mon Sep 17 00:00:00 2001 From: codytseng Date: Sun, 9 Mar 2025 15:28:12 +0800 Subject: [PATCH] feat: cache post content --- .../PostEditor/NormalPostContent.tsx | 24 ++++++++++++-- src/components/PostEditor/index.tsx | 4 +-- .../TextareaWithMentions.tsx/index.tsx | 11 +++++++ src/services/post-content-cache.service.ts | 32 +++++++++++++++++++ 4 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 src/services/post-content-cache.service.ts diff --git a/src/components/PostEditor/NormalPostContent.tsx b/src/components/PostEditor/NormalPostContent.tsx index 0f2faae0..048c8462 100644 --- a/src/components/PostEditor/NormalPostContent.tsx +++ b/src/components/PostEditor/NormalPostContent.tsx @@ -4,9 +4,10 @@ import { createCommentDraftEvent, createShortTextNoteDraftEvent } from '@/lib/dr import { getRootEventTag } from '@/lib/event.ts' import { useNostr } from '@/providers/NostrProvider' import client from '@/services/client.service' +import postContentCache from '@/services/post-content-cache.service' import { ChevronDown, ImageUp, LoaderCircle } from 'lucide-react' import { Event, kinds, nip19 } from 'nostr-tools' -import { useState } from 'react' +import { useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import TextareaWithMentions from '../TextareaWithMentions.tsx' import Mentions from './Mentions' @@ -27,7 +28,7 @@ export default function NormalPostContent({ const { t } = useTranslation() const { toast } = useToast() const { publish, checkLogin } = useNostr() - const [content, setContent] = useState(defaultContent) + const [content, setContent] = useState('') const [pictureInfos, setPictureInfos] = useState<{ url: string; tags: string[][] }[]>([]) const [posting, setPosting] = useState(false) const [showMoreOptions, setShowMoreOptions] = useState(false) @@ -35,8 +36,26 @@ export default function NormalPostContent({ const [specifiedRelayUrls, setSpecifiedRelayUrls] = useState(undefined) const [uploadingPicture, setUploadingPicture] = useState(false) const [mentions, setMentions] = useState([]) + const [cursorOffset, setCursorOffset] = useState(0) + const initializedRef = useRef(false) const canPost = !!content && !posting + useEffect(() => { + const cachedContent = postContentCache.get({ defaultContent, parentEvent }) + if (cachedContent) { + setContent(cachedContent) + } + if (defaultContent) { + setCursorOffset(defaultContent.length) + } + initializedRef.current = true + }, [defaultContent, parentEvent]) + + useEffect(() => { + if (!initializedRef.current) return + postContentCache.set({ defaultContent, parentEvent }, content) + }, [content]) + const post = async (e: React.MouseEvent) => { e.stopPropagation() checkLogin(async () => { @@ -130,6 +149,7 @@ export default function NormalPostContent({ setTextValue={setContent} textValue={content} placeholder={t('Write something...')} + cursorOffset={cursorOffset} /> {content && } ) - }, [parentEvent]) + }, [parentEvent, defaultContent]) if (isSmallScreen) { return ( diff --git a/src/components/TextareaWithMentions.tsx/index.tsx b/src/components/TextareaWithMentions.tsx/index.tsx index 75b5076f..da17bcdc 100644 --- a/src/components/TextareaWithMentions.tsx/index.tsx +++ b/src/components/TextareaWithMentions.tsx/index.tsx @@ -21,10 +21,12 @@ import { getCurrentWord, replaceWord } from './utils' export default function TextareaWithMentions({ textValue, setTextValue, + cursorOffset = 0, ...props }: ComponentProps<'textarea'> & { textValue: string setTextValue: Dispatch> + cursorOffset?: number }) { const textareaRef = useRef(null) const dropdownRef = useRef(null) @@ -33,6 +35,15 @@ export default function TextareaWithMentions({ const [debouncedCommandValue, setDebouncedCommandValue] = useState(commandValue) const [profiles, setProfiles] = useState([]) + useEffect(() => { + if (textareaRef.current && cursorOffset !== 0) { + const textarea = textareaRef.current + const newPos = Math.max(0, textarea.selectionStart - cursorOffset) + textarea.setSelectionRange(newPos, newPos) + textarea.focus() + } + }, [cursorOffset]) + useEffect(() => { const handler = setTimeout(() => { setDebouncedCommandValue(commandValue) diff --git a/src/services/post-content-cache.service.ts b/src/services/post-content-cache.service.ts new file mode 100644 index 00000000..24f165fb --- /dev/null +++ b/src/services/post-content-cache.service.ts @@ -0,0 +1,32 @@ +import { Event } from 'nostr-tools' + +class PostContentCacheService { + static instance: PostContentCacheService + + private cache: Map = new Map() + + constructor() { + if (!PostContentCacheService.instance) { + PostContentCacheService.instance = this + } + return PostContentCacheService.instance + } + + get({ defaultContent, parentEvent }: { defaultContent?: string; parentEvent?: Event } = {}) { + return this.cache.get(this.generateCacheKey(defaultContent, parentEvent)) ?? defaultContent + } + + set( + { defaultContent, parentEvent }: { defaultContent?: string; parentEvent?: Event }, + content: string + ) { + this.cache.set(this.generateCacheKey(defaultContent, parentEvent), content) + } + + generateCacheKey(defaultContent: string = '', parentEvent?: Event): string { + return parentEvent ? parentEvent.id : defaultContent + } +} + +const instance = new PostContentCacheService() +export default instance