35 changed files with 1240 additions and 130 deletions
@ -0,0 +1,232 @@
@@ -0,0 +1,232 @@
|
||||
import { Button } from '@/components/ui/button' |
||||
import { POLL_TYPE } from '@/constants' |
||||
import { useFetchPollResults } from '@/hooks/useFetchPollResults' |
||||
import { createPollResponseDraftEvent } from '@/lib/draft-event' |
||||
import { getPollMetadataFromEvent } from '@/lib/event-metadata' |
||||
import { cn } from '@/lib/utils' |
||||
import { useNostr } from '@/providers/NostrProvider' |
||||
import client from '@/services/client.service' |
||||
import pollResultsService from '@/services/poll-results.service' |
||||
import dayjs from 'dayjs' |
||||
import { CheckCircle2, Loader2 } from 'lucide-react' |
||||
import { Event } from 'nostr-tools' |
||||
import { useMemo, useState } from 'react' |
||||
import { useTranslation } from 'react-i18next' |
||||
import { toast } from 'sonner' |
||||
|
||||
export default function Poll({ event, className }: { event: Event; className?: string }) { |
||||
const { t } = useTranslation() |
||||
const { pubkey, publish, startLogin } = useNostr() |
||||
const [isVoting, setIsVoting] = useState(false) |
||||
const [selectedOptionIds, setSelectedOptionIds] = useState<string[]>([]) |
||||
const pollResults = useFetchPollResults(event.id) |
||||
const [isLoadingResults, setIsLoadingResults] = useState(false) |
||||
const poll = useMemo(() => getPollMetadataFromEvent(event), [event]) |
||||
const votedOptionIds = useMemo(() => { |
||||
if (!pollResults || !pubkey) return [] |
||||
return Object.entries(pollResults.results) |
||||
.filter(([, voters]) => voters.has(pubkey)) |
||||
.map(([optionId]) => optionId) |
||||
}, [pollResults, pubkey]) |
||||
const validPollOptionIds = useMemo(() => poll?.options.map((option) => option.id) || [], [poll]) |
||||
const isExpired = useMemo(() => poll?.endsAt && dayjs().unix() > poll.endsAt, [poll]) |
||||
const isMultipleChoice = useMemo(() => poll?.pollType === POLL_TYPE.MULTIPLE_CHOICE, [poll]) |
||||
const canVote = useMemo(() => !isExpired && !votedOptionIds.length, [isExpired, votedOptionIds]) |
||||
|
||||
if (!poll) { |
||||
return null |
||||
} |
||||
|
||||
const fetchResults = async () => { |
||||
setIsLoadingResults(true) |
||||
try { |
||||
const relays = await ensurePollRelays(event.pubkey, poll) |
||||
return await pollResultsService.fetchResults( |
||||
event.id, |
||||
relays, |
||||
validPollOptionIds, |
||||
isMultipleChoice, |
||||
poll.endsAt |
||||
) |
||||
} catch (error) { |
||||
console.error('Failed to fetch poll results:', error) |
||||
toast.error('Failed to fetch poll results: ' + (error as Error).message) |
||||
} finally { |
||||
setIsLoadingResults(false) |
||||
} |
||||
} |
||||
|
||||
const handleOptionClick = (optionId: string) => { |
||||
if (isExpired) return |
||||
|
||||
if (isMultipleChoice) { |
||||
setSelectedOptionIds((prev) => |
||||
prev.includes(optionId) ? prev.filter((id) => id !== optionId) : [...prev, optionId] |
||||
) |
||||
} else { |
||||
setSelectedOptionIds((prev) => (prev.includes(optionId) ? [] : [optionId])) |
||||
} |
||||
} |
||||
|
||||
const handleVote = async () => { |
||||
if (selectedOptionIds.length === 0) return |
||||
if (!pubkey) { |
||||
startLogin() |
||||
return |
||||
} |
||||
|
||||
setIsVoting(true) |
||||
try { |
||||
if (!pollResults) { |
||||
const _pollResults = await fetchResults() |
||||
if (_pollResults && _pollResults.voters.has(pubkey)) { |
||||
return |
||||
} |
||||
} |
||||
|
||||
const additionalRelayUrls = await ensurePollRelays(event.pubkey, poll) |
||||
|
||||
const draftEvent = createPollResponseDraftEvent(event, selectedOptionIds) |
||||
await publish(draftEvent, { |
||||
additionalRelayUrls |
||||
}) |
||||
|
||||
setSelectedOptionIds([]) |
||||
pollResultsService.addPollResponse(event.id, pubkey, selectedOptionIds) |
||||
} catch (error) { |
||||
console.error('Failed to vote:', error) |
||||
toast.error('Failed to vote: ' + (error as Error).message) |
||||
} finally { |
||||
setIsVoting(false) |
||||
} |
||||
} |
||||
|
||||
return ( |
||||
<div className={className}> |
||||
<div className="space-y-2"> |
||||
<div className="text-sm text-muted-foreground"> |
||||
{poll.pollType === POLL_TYPE.MULTIPLE_CHOICE && ( |
||||
<p>{t('Multiple choice (select one or more)')}</p> |
||||
)} |
||||
</div> |
||||
|
||||
{/* Poll Options */} |
||||
<div className="grid gap-2"> |
||||
{poll.options.map((option) => { |
||||
const votes = pollResults?.results?.[option.id]?.size ?? 0 |
||||
const totalVotes = pollResults?.totalVotes ?? 0 |
||||
const percentage = totalVotes > 0 ? (votes / totalVotes) * 100 : 0 |
||||
const isMax = |
||||
pollResults && pollResults.totalVotes > 0 |
||||
? Object.values(pollResults.results).every((res) => res.size <= votes) |
||||
: false |
||||
|
||||
return ( |
||||
<button |
||||
key={option.id} |
||||
title={option.label} |
||||
className={cn( |
||||
'relative w-full px-4 py-3 rounded-lg border transition-all flex items-center gap-2', |
||||
canVote ? 'cursor-pointer' : 'cursor-not-allowed', |
||||
canVote && |
||||
(selectedOptionIds.includes(option.id) |
||||
? 'border-primary bg-primary/20' |
||||
: 'hover:border-primary/40 hover:bg-primary/5') |
||||
)} |
||||
onClick={(e) => { |
||||
e.stopPropagation() |
||||
handleOptionClick(option.id) |
||||
}} |
||||
disabled={!canVote} |
||||
> |
||||
{/* Content */} |
||||
<div className="flex items-center gap-2 flex-1 w-0 z-10"> |
||||
<div className={cn('line-clamp-2 text-left', isMax ? 'font-semibold' : '')}> |
||||
{option.label} |
||||
</div> |
||||
{votedOptionIds.includes(option.id) && ( |
||||
<CheckCircle2 className="size-4 shrink-0" /> |
||||
)} |
||||
</div> |
||||
{!!pollResults && ( |
||||
<div |
||||
className={cn( |
||||
'text-muted-foreground shrink-0 z-10', |
||||
isMax ? 'font-semibold text-foreground' : '' |
||||
)} |
||||
> |
||||
{percentage.toFixed(1)}% |
||||
</div> |
||||
)} |
||||
|
||||
{/* Progress Bar Background */} |
||||
<div |
||||
className={cn( |
||||
'absolute inset-0 rounded-md transition-all duration-700 ease-out', |
||||
isMax ? 'bg-primary/60' : 'bg-muted/90' |
||||
)} |
||||
style={{ width: `${percentage}%` }} |
||||
/> |
||||
</button> |
||||
) |
||||
})} |
||||
</div> |
||||
|
||||
{/* Results Summary */} |
||||
<div className="text-sm text-muted-foreground"> |
||||
{!!pollResults && t('{{number}} votes', { number: pollResults.totalVotes ?? 0 })} |
||||
{!!pollResults && !!poll.endsAt && ' · '} |
||||
{!!poll.endsAt && |
||||
(isExpired |
||||
? t('Poll has ended') |
||||
: t('Poll ends at {{time}}', { |
||||
time: new Date(poll.endsAt * 1000).toLocaleString() |
||||
}))} |
||||
</div> |
||||
|
||||
{(canVote || !pollResults) && ( |
||||
<div className="flex items-center justify-between gap-2"> |
||||
{/* Vote Button */} |
||||
{canVote && ( |
||||
<Button |
||||
onClick={(e) => { |
||||
e.stopPropagation() |
||||
if (selectedOptionIds.length === 0) return |
||||
handleVote() |
||||
}} |
||||
disabled={!selectedOptionIds.length || isVoting} |
||||
className="flex-1" |
||||
> |
||||
{isVoting && <Loader2 className="animate-spin" />} |
||||
{t('Vote')} |
||||
</Button> |
||||
)} |
||||
|
||||
{!pollResults && ( |
||||
<Button |
||||
variant="secondary" |
||||
onClick={(e) => { |
||||
e.stopPropagation() |
||||
fetchResults() |
||||
}} |
||||
disabled={isLoadingResults} |
||||
> |
||||
{isLoadingResults && <Loader2 className="animate-spin" />} |
||||
{t('Load results')} |
||||
</Button> |
||||
)} |
||||
</div> |
||||
)} |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
async function ensurePollRelays(creator: string, poll: { relayUrls: string[] }) { |
||||
const relays = poll.relayUrls.slice(0, 4) |
||||
if (!relays.length) { |
||||
const relayList = await client.fetchRelayList(creator) |
||||
relays.push(...relayList.read.slice(0, 4)) |
||||
} |
||||
return relays |
||||
} |
||||
@ -0,0 +1,145 @@
@@ -0,0 +1,145 @@
|
||||
import { Button } from '@/components/ui/button' |
||||
import { Input } from '@/components/ui/input' |
||||
import { Label } from '@/components/ui/label' |
||||
import { Switch } from '@/components/ui/switch' |
||||
import { normalizeUrl } from '@/lib/url' |
||||
import { TPollCreateData } from '@/types' |
||||
import dayjs from 'dayjs' |
||||
import { AlertCircle, Eraser, X } from 'lucide-react' |
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react' |
||||
import { useTranslation } from 'react-i18next' |
||||
|
||||
export default function PollEditor({ |
||||
pollCreateData, |
||||
setPollCreateData, |
||||
setIsPoll |
||||
}: { |
||||
pollCreateData: TPollCreateData |
||||
setPollCreateData: Dispatch<SetStateAction<TPollCreateData>> |
||||
setIsPoll: Dispatch<SetStateAction<boolean>> |
||||
}) { |
||||
const { t } = useTranslation() |
||||
const [isMultipleChoice, setIsMultipleChoice] = useState(pollCreateData.isMultipleChoice) |
||||
const [options, setOptions] = useState(pollCreateData.options) |
||||
const [endsAt, setEndsAt] = useState( |
||||
pollCreateData.endsAt ? dayjs(pollCreateData.endsAt * 1000).format('YYYY-MM-DDTHH:mm') : '' |
||||
) |
||||
const [relayUrls, setRelayUrls] = useState(pollCreateData.relays.join(', ')) |
||||
|
||||
useEffect(() => { |
||||
setPollCreateData({ |
||||
isMultipleChoice, |
||||
options, |
||||
endsAt: endsAt ? dayjs(endsAt).startOf('minute').unix() : undefined, |
||||
relays: relayUrls |
||||
? relayUrls |
||||
.split(',') |
||||
.map((url) => normalizeUrl(url.trim())) |
||||
.filter(Boolean) |
||||
: [] |
||||
}) |
||||
}, [isMultipleChoice, options, endsAt, relayUrls]) |
||||
|
||||
const handleAddOption = () => { |
||||
setOptions([...options, '']) |
||||
} |
||||
|
||||
const handleRemoveOption = (index: number) => { |
||||
if (options.length > 2) { |
||||
setOptions(options.filter((_, i) => i !== index)) |
||||
} |
||||
} |
||||
|
||||
const handleOptionChange = (index: number, value: string) => { |
||||
const newOptions = [...options] |
||||
newOptions[index] = value |
||||
setOptions(newOptions) |
||||
} |
||||
|
||||
return ( |
||||
<div className="space-y-4 border rounded-lg p-3"> |
||||
<div className="space-y-2"> |
||||
{options.map((option, index) => ( |
||||
<div key={index} className="flex gap-2"> |
||||
<Input |
||||
value={option} |
||||
onChange={(e) => handleOptionChange(index, e.target.value)} |
||||
placeholder={t('Option {{number}}', { number: index + 1 })} |
||||
/> |
||||
<Button |
||||
type="button" |
||||
variant="ghost-destructive" |
||||
size="icon" |
||||
onClick={() => handleRemoveOption(index)} |
||||
disabled={options.length <= 2} |
||||
> |
||||
<X /> |
||||
</Button> |
||||
</div> |
||||
))} |
||||
<Button type="button" variant="outline" onClick={handleAddOption}> |
||||
{t('Add Option')} |
||||
</Button> |
||||
</div> |
||||
|
||||
<div className="flex items-center space-x-2"> |
||||
<Label htmlFor="multiple-choice">{t('Allow multiple choices')}</Label> |
||||
<Switch |
||||
id="multiple-choice" |
||||
checked={isMultipleChoice} |
||||
onCheckedChange={setIsMultipleChoice} |
||||
/> |
||||
</div> |
||||
|
||||
<div className="grid gap-2"> |
||||
<Label htmlFor="ends-at">{t('End Date (optional)')}</Label> |
||||
<div className="flex items-center gap-2"> |
||||
<Input |
||||
id="ends-at" |
||||
type="datetime-local" |
||||
value={endsAt} |
||||
onChange={(e) => setEndsAt(e.target.value)} |
||||
/> |
||||
<Button |
||||
type="button" |
||||
variant="ghost-destructive" |
||||
size="icon" |
||||
onClick={() => setEndsAt('')} |
||||
disabled={!endsAt} |
||||
title={t('Clear end date')} |
||||
> |
||||
<Eraser /> |
||||
</Button> |
||||
</div> |
||||
</div> |
||||
|
||||
<div className="grid gap-2"> |
||||
<Label htmlFor="relay-urls">{t('Relay URLs (optional, comma-separated)')}</Label> |
||||
<Input |
||||
id="relay-urls" |
||||
value={relayUrls} |
||||
onChange={(e) => setRelayUrls(e.target.value)} |
||||
placeholder="wss://relay1.com, wss://relay2.com" |
||||
/> |
||||
</div> |
||||
|
||||
<div className="grid gap-2"> |
||||
<div className="p-3 rounded-lg text-sm bg-destructive [&_svg]:size-4"> |
||||
<div className="flex items-center gap-2"> |
||||
<AlertCircle /> |
||||
<div className="font-medium">{t('This is a poll note.')}</div> |
||||
</div> |
||||
<div className="pl-6"> |
||||
{t( |
||||
'Unlike regular notes, polls are not widely supported and may not display on other clients.' |
||||
)} |
||||
</div> |
||||
</div> |
||||
|
||||
<Button variant="ghost-destructive" className="w-full" onClick={() => setIsPoll(false)}> |
||||
{t('Remove poll')} |
||||
</Button> |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
import pollResults from '@/services/poll-results.service' |
||||
import { useSyncExternalStore } from 'react' |
||||
|
||||
export function useFetchPollResults(pollEventId: string) { |
||||
return useSyncExternalStore( |
||||
(cb) => pollResults.subscribePollResults(pollEventId, cb), |
||||
() => pollResults.getPollResults(pollEventId) |
||||
) |
||||
} |
||||
@ -0,0 +1,142 @@
@@ -0,0 +1,142 @@
|
||||
import { ExtendedKind } from '@/constants' |
||||
import { getPollResponseFromEvent } from '@/lib/event-metadata' |
||||
import dayjs from 'dayjs' |
||||
import { Filter } from 'nostr-tools' |
||||
import client from './client.service' |
||||
|
||||
export type TPollResults = { |
||||
totalVotes: number |
||||
results: Record<string, Set<string>> |
||||
voters: Set<string> |
||||
updatedAt: number |
||||
} |
||||
|
||||
class PollResultsService { |
||||
static instance: PollResultsService |
||||
private pollResultsMap: Map<string, TPollResults> = new Map() |
||||
private pollResultsSubscribers = new Map<string, Set<() => void>>() |
||||
|
||||
constructor() { |
||||
if (!PollResultsService.instance) { |
||||
PollResultsService.instance = this |
||||
} |
||||
return PollResultsService.instance |
||||
} |
||||
|
||||
async fetchResults( |
||||
pollEventId: string, |
||||
relays: string[], |
||||
validPollOptionIds: string[], |
||||
isMultipleChoice: boolean, |
||||
endsAt?: number |
||||
) { |
||||
const filter: Filter = { |
||||
kinds: [ExtendedKind.POLL_RESPONSE], |
||||
'#e': [pollEventId], |
||||
limit: 1000 |
||||
} |
||||
|
||||
if (endsAt) { |
||||
filter.until = endsAt |
||||
} |
||||
|
||||
let results = this.pollResultsMap.get(pollEventId) |
||||
if (results) { |
||||
if (endsAt && results.updatedAt >= endsAt) { |
||||
return results |
||||
} |
||||
filter.since = results.updatedAt |
||||
} else { |
||||
results = { |
||||
totalVotes: 0, |
||||
results: validPollOptionIds.reduce( |
||||
(acc, optionId) => { |
||||
acc[optionId] = new Set<string>() |
||||
return acc |
||||
}, |
||||
{} as Record<string, Set<string>> |
||||
), |
||||
voters: new Set<string>(), |
||||
updatedAt: 0 |
||||
} |
||||
} |
||||
|
||||
const responseEvents = await client.fetchEvents(relays, { |
||||
kinds: [ExtendedKind.POLL_RESPONSE], |
||||
'#e': [pollEventId], |
||||
limit: 1000 |
||||
}) |
||||
|
||||
results.updatedAt = dayjs().unix() |
||||
|
||||
const responses = responseEvents |
||||
.map((evt) => getPollResponseFromEvent(evt, validPollOptionIds, isMultipleChoice)) |
||||
.filter((response): response is NonNullable<typeof response> => response !== null) |
||||
|
||||
responses |
||||
.sort((a, b) => b.created_at - a.created_at) |
||||
.forEach((response) => { |
||||
if (results && results.voters.has(response.pubkey)) return |
||||
results.voters.add(response.pubkey) |
||||
|
||||
results.totalVotes += response.selectedOptionIds.length |
||||
response.selectedOptionIds.forEach((optionId) => { |
||||
if (results.results[optionId]) { |
||||
results.results[optionId].add(response.pubkey) |
||||
} |
||||
}) |
||||
}) |
||||
|
||||
this.pollResultsMap.set(pollEventId, { ...results }) |
||||
if (responseEvents.length) { |
||||
this.notifyPollResults(pollEventId) |
||||
} |
||||
return results |
||||
} |
||||
|
||||
subscribePollResults(pollEventId: string, callback: () => void) { |
||||
let set = this.pollResultsSubscribers.get(pollEventId) |
||||
if (!set) { |
||||
set = new Set() |
||||
this.pollResultsSubscribers.set(pollEventId, set) |
||||
} |
||||
set.add(callback) |
||||
return () => { |
||||
set?.delete(callback) |
||||
if (set?.size === 0) this.pollResultsSubscribers.delete(pollEventId) |
||||
} |
||||
} |
||||
|
||||
private notifyPollResults(pollEventId: string) { |
||||
const set = this.pollResultsSubscribers.get(pollEventId) |
||||
if (set) { |
||||
set.forEach((cb) => cb()) |
||||
} |
||||
} |
||||
|
||||
getPollResults(id: string): TPollResults | undefined { |
||||
return this.pollResultsMap.get(id) |
||||
} |
||||
|
||||
addPollResponse(pollEventId: string, pubkey: string, selectedOptionIds: string[]) { |
||||
const results = this.pollResultsMap.get(pollEventId) |
||||
if (!results) return |
||||
|
||||
if (results.voters.has(pubkey)) return |
||||
|
||||
results.voters.add(pubkey) |
||||
results.totalVotes += selectedOptionIds.length |
||||
selectedOptionIds.forEach((optionId) => { |
||||
if (results.results[optionId]) { |
||||
results.results[optionId].add(pubkey) |
||||
} |
||||
}) |
||||
|
||||
this.pollResultsMap.set(pollEventId, { ...results }) |
||||
this.notifyPollResults(pollEventId) |
||||
} |
||||
} |
||||
|
||||
const instance = new PollResultsService() |
||||
|
||||
export default instance |
||||
@ -1,48 +0,0 @@
@@ -1,48 +0,0 @@
|
||||
import { Content } from '@tiptap/react' |
||||
import { Event } from 'nostr-tools' |
||||
|
||||
class PostContentCacheService { |
||||
static instance: PostContentCacheService |
||||
|
||||
private normalPostCache: Map<string, Content> = new Map() |
||||
|
||||
constructor() { |
||||
if (!PostContentCacheService.instance) { |
||||
PostContentCacheService.instance = this |
||||
} |
||||
return PostContentCacheService.instance |
||||
} |
||||
|
||||
getPostCache({ |
||||
defaultContent, |
||||
parentEvent |
||||
}: { defaultContent?: string; parentEvent?: Event } = {}) { |
||||
return ( |
||||
this.normalPostCache.get(this.generateCacheKey(defaultContent, parentEvent)) ?? defaultContent |
||||
) |
||||
} |
||||
|
||||
setPostCache( |
||||
{ defaultContent, parentEvent }: { defaultContent?: string; parentEvent?: Event }, |
||||
content: Content |
||||
) { |
||||
this.normalPostCache.set(this.generateCacheKey(defaultContent, parentEvent), content) |
||||
} |
||||
|
||||
clearPostCache({ |
||||
defaultContent, |
||||
parentEvent |
||||
}: { |
||||
defaultContent?: string |
||||
parentEvent?: Event |
||||
}) { |
||||
this.normalPostCache.delete(this.generateCacheKey(defaultContent, parentEvent)) |
||||
} |
||||
|
||||
generateCacheKey(defaultContent: string = '', parentEvent?: Event): string { |
||||
return parentEvent ? parentEvent.id : defaultContent |
||||
} |
||||
} |
||||
|
||||
const instance = new PostContentCacheService() |
||||
export default instance |
||||
@ -0,0 +1,75 @@
@@ -0,0 +1,75 @@
|
||||
import { TPollCreateData } from '@/types' |
||||
import { Content } from '@tiptap/react' |
||||
import { Event } from 'nostr-tools' |
||||
|
||||
type TPostSettings = { |
||||
isNsfw?: boolean |
||||
isPoll?: boolean |
||||
pollCreateData?: TPollCreateData |
||||
specifiedRelayUrls?: string[] |
||||
addClientTag?: boolean |
||||
} |
||||
|
||||
class PostEditorCacheService { |
||||
static instance: PostEditorCacheService |
||||
|
||||
private postContentCache: Map<string, Content> = new Map() |
||||
private postSettingsCache: Map<string, TPostSettings> = new Map() |
||||
|
||||
constructor() { |
||||
if (!PostEditorCacheService.instance) { |
||||
PostEditorCacheService.instance = this |
||||
} |
||||
return PostEditorCacheService.instance |
||||
} |
||||
|
||||
getPostContentCache({ |
||||
defaultContent, |
||||
parentEvent |
||||
}: { defaultContent?: string; parentEvent?: Event } = {}) { |
||||
return ( |
||||
this.postContentCache.get(this.generateCacheKey(defaultContent, parentEvent)) ?? |
||||
defaultContent |
||||
) |
||||
} |
||||
|
||||
setPostContentCache( |
||||
{ defaultContent, parentEvent }: { defaultContent?: string; parentEvent?: Event }, |
||||
content: Content |
||||
) { |
||||
this.postContentCache.set(this.generateCacheKey(defaultContent, parentEvent), content) |
||||
} |
||||
|
||||
getPostSettingsCache({ |
||||
defaultContent, |
||||
parentEvent |
||||
}: { defaultContent?: string; parentEvent?: Event } = {}): TPostSettings | undefined { |
||||
return this.postSettingsCache.get(this.generateCacheKey(defaultContent, parentEvent)) |
||||
} |
||||
|
||||
setPostSettingsCache( |
||||
{ defaultContent, parentEvent }: { defaultContent?: string; parentEvent?: Event }, |
||||
settings: TPostSettings |
||||
) { |
||||
this.postSettingsCache.set(this.generateCacheKey(defaultContent, parentEvent), settings) |
||||
} |
||||
|
||||
clearPostCache({ |
||||
defaultContent, |
||||
parentEvent |
||||
}: { |
||||
defaultContent?: string |
||||
parentEvent?: Event |
||||
}) { |
||||
const cacheKey = this.generateCacheKey(defaultContent, parentEvent) |
||||
this.postContentCache.delete(cacheKey) |
||||
this.postSettingsCache.delete(cacheKey) |
||||
} |
||||
|
||||
generateCacheKey(defaultContent: string = '', parentEvent?: Event): string { |
||||
return parentEvent ? parentEvent.id : defaultContent |
||||
} |
||||
} |
||||
|
||||
const instance = new PostEditorCacheService() |
||||
export default instance |
||||
Loading…
Reference in new issue