From ed135d3dabcebe12875a8576590894632e07aea6 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Wed, 29 Oct 2025 13:18:38 +0100 Subject: [PATCH] quiet and expiration tags implemented --- src/components/NoteInteractions/index.tsx | 6 + src/components/NoteList/index.tsx | 4 + src/components/NoteStats/LikeButton.tsx | 6 +- src/components/NoteStats/ReplyButton.tsx | 4 +- src/components/NoteStats/RepostButton.tsx | 4 +- src/components/NoteStats/ZapButton.tsx | 4 +- src/components/NoteStats/index.tsx | 20 ++-- src/components/PostEditor/PostContent.tsx | 43 ++++++- src/constants.ts | 6 + src/lib/draft-event.ts | 88 +++++++++++++- src/lib/event-filtering.ts | 62 ++++++++++ .../PostSettingsPage/ExpirationSettings.tsx | 66 +++++++++++ .../PostSettingsPage/QuietSettings.tsx | 108 ++++++++++++++++++ .../secondary/PostSettingsPage/index.tsx | 12 +- src/services/local-storage.service.ts | 95 +++++++++++++++ 15 files changed, 503 insertions(+), 25 deletions(-) create mode 100644 src/lib/event-filtering.ts create mode 100644 src/pages/secondary/PostSettingsPage/ExpirationSettings.tsx create mode 100644 src/pages/secondary/PostSettingsPage/QuietSettings.tsx diff --git a/src/components/NoteInteractions/index.tsx b/src/components/NoteInteractions/index.tsx index 678687a..c9ab644 100644 --- a/src/components/NoteInteractions/index.tsx +++ b/src/components/NoteInteractions/index.tsx @@ -1,6 +1,7 @@ import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' import { Separator } from '@/components/ui/separator' import { ExtendedKind } from '@/constants' +import { shouldHideInteractions } from '@/lib/event-filtering' import { Event } from 'nostr-tools' import { useState } from 'react' import HideUntrustedContentButton from '../HideUntrustedContentButton' @@ -23,6 +24,11 @@ export default function NoteInteractions({ const [replySort, setReplySort] = useState('oldest') const isDiscussion = event.kind === ExtendedKind.DISCUSSION + // Hide interactions if event is in quiet mode + if (shouldHideInteractions(event)) { + return null + } + let list switch (type) { case 'replies': diff --git a/src/components/NoteList/index.tsx b/src/components/NoteList/index.tsx index 2c8c176..2c5d195 100644 --- a/src/components/NoteList/index.tsx +++ b/src/components/NoteList/index.tsx @@ -6,6 +6,7 @@ import { isReplaceableEvent, isReplyNoteEvent } from '@/lib/event' +import { shouldFilterEvent } from '@/lib/event-filtering' import { isTouchDevice } from '@/lib/utils' import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useDeletedEvent } from '@/providers/DeletedEventProvider' @@ -102,6 +103,9 @@ const NoteList = forwardRef( return true } + // Filter out expired events + if (shouldFilterEvent(evt)) return true + return false }, [hideReplies, hideUntrustedNotes, mutePubkeySet, pinnedEventIds, isEventDeleted] diff --git a/src/components/NoteStats/LikeButton.tsx b/src/components/NoteStats/LikeButton.tsx index 70edb85..cd3f16e 100644 --- a/src/components/NoteStats/LikeButton.tsx +++ b/src/components/NoteStats/LikeButton.tsx @@ -24,7 +24,7 @@ import SuggestedEmojis from '../SuggestedEmojis' import DiscussionEmojis from '../SuggestedEmojis/DiscussionEmojis' import { formatCount } from './utils' -export default function LikeButton({ event }: { event: Event }) { +export default function LikeButton({ event, hideCount = false }: { event: Event; hideCount?: boolean }) { const { t } = useTranslation() const { isSmallScreen } = useScreenSize() const { pubkey, publish, checkLogin } = useNostr() @@ -144,12 +144,12 @@ export default function LikeButton({ event }: { event: Event }) { ) : myLastEmoji ? ( <> - {!!likeCount &&
{formatCount(likeCount)}
} + {!hideCount && !!likeCount &&
{formatCount(likeCount)}
} ) : ( <> - {!!likeCount &&
{formatCount(likeCount)}
} + {!hideCount && !!likeCount &&
{formatCount(likeCount)}
} )} diff --git a/src/components/NoteStats/ReplyButton.tsx b/src/components/NoteStats/ReplyButton.tsx index 016b5eb..6ae9aba 100644 --- a/src/components/NoteStats/ReplyButton.tsx +++ b/src/components/NoteStats/ReplyButton.tsx @@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next' import PostEditor from '../PostEditor' import { formatCount } from './utils' -export default function ReplyButton({ event }: { event: Event }) { +export default function ReplyButton({ event, hideCount = false }: { event: Event; hideCount?: boolean }) { const { t } = useTranslation() const { pubkey, checkLogin } = useNostr() const noteStats = useNoteStatsById(event.id) @@ -44,7 +44,7 @@ export default function ReplyButton({ event }: { event: Event }) { title={t('Reply')} > - {!!replyCount &&
{formatCount(replyCount)}
} + {!hideCount && !!replyCount &&
{formatCount(replyCount)}
} diff --git a/src/components/NoteStats/RepostButton.tsx b/src/components/NoteStats/RepostButton.tsx index d2462b7..41a0352 100644 --- a/src/components/NoteStats/RepostButton.tsx +++ b/src/components/NoteStats/RepostButton.tsx @@ -21,7 +21,7 @@ import { useTranslation } from 'react-i18next' import PostEditor from '../PostEditor' import { formatCount } from './utils' -export default function RepostButton({ event }: { event: Event }) { +export default function RepostButton({ event, hideCount = false }: { event: Event; hideCount?: boolean }) { const { t } = useTranslation() const { isSmallScreen } = useScreenSize() const { hideUntrustedInteractions, isUserTrusted } = useUserTrust() @@ -82,7 +82,7 @@ export default function RepostButton({ event }: { event: Event }) { }} > {reposting ? : } - {!!repostCount &&
{formatCount(repostCount)}
} + {!hideCount && !!repostCount &&
{formatCount(repostCount)}
} ) diff --git a/src/components/NoteStats/ZapButton.tsx b/src/components/NoteStats/ZapButton.tsx index 24ef403..9ae7787 100644 --- a/src/components/NoteStats/ZapButton.tsx +++ b/src/components/NoteStats/ZapButton.tsx @@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next' import { toast } from 'sonner' import ZapDialog from '../ZapDialog' -export default function ZapButton({ event }: { event: Event }) { +export default function ZapButton({ event, hideCount = false }: { event: Event; hideCount?: boolean }) { const { t } = useTranslation() const { checkLogin, pubkey } = useNostr() const noteStats = useNoteStatsById(event.id) @@ -147,7 +147,7 @@ export default function ZapButton({ event }: { event: Event }) { ) : ( )} - {!!zapAmount &&
{formatAmount(zapAmount)}
} + {!hideCount && !!zapAmount &&
{formatAmount(zapAmount)}
} { if (isDiscussion) return // Already a discussion event @@ -80,10 +84,10 @@ export default function NoteStats({ classNames?.buttonBar )} > - - {!isDiscussion && !isReplyToDiscussion && } - - + + {!isDiscussion && !isReplyToDiscussion && } + + @@ -103,10 +107,10 @@ export default function NoteStats({
- - {!isDiscussion && !isReplyToDiscussion && } - - + + {!isDiscussion && !isReplyToDiscussion && } + +
diff --git a/src/components/PostEditor/PostContent.tsx b/src/components/PostEditor/PostContent.tsx index f06970b..40f84b3 100644 --- a/src/components/PostEditor/PostContent.tsx +++ b/src/components/PostEditor/PostContent.tsx @@ -17,6 +17,7 @@ import { useFeed } from '@/providers/FeedProvider' import { useReply } from '@/providers/ReplyProvider' import { normalizeUrl, cleanUrl } from '@/lib/url' import postEditorCache from '@/services/post-editor-cache.service' +import storage from '@/services/local-storage.service' import { TPollCreateData } from '@/types' import { ImageUp, ListTodo, LoaderCircle, MessageCircle, Settings, Smile, X, Highlighter } from 'lucide-react' import { Event, kinds } from 'nostr-tools' @@ -202,6 +203,12 @@ export default function PostContent({ } ) + // Get expiration and quiet settings + const addExpirationTag = storage.getDefaultExpirationEnabled() + const expirationMonths = storage.getDefaultExpirationMonths() + const addQuietTag = storage.getDefaultQuietEnabled() + const quietDays = storage.getDefaultQuietDays() + if (isHighlight) { // For highlights, pass the original sourceValue which contains the full identifier // The createHighlightDraftEvent function will parse it correctly @@ -213,36 +220,60 @@ export default function PostContent({ undefined, // description parameter (not used) { addClientTag, - isNsfw + isNsfw, + addExpirationTag, + expirationMonths, + addQuietTag, + quietDays } ) } else if (isPublicMessage) { draftEvent = await createPublicMessageDraftEvent(cleanedText, extractedMentions, { addClientTag, - isNsfw + isNsfw, + addExpirationTag, + expirationMonths, + addQuietTag, + quietDays }) } else if (parentEvent && parentEvent.kind === ExtendedKind.PUBLIC_MESSAGE) { draftEvent = await createPublicMessageReplyDraftEvent(cleanedText, parentEvent, mentions, { addClientTag, - isNsfw + isNsfw, + addExpirationTag, + expirationMonths, + addQuietTag, + quietDays }) } else if (parentEvent && parentEvent.kind !== kinds.ShortTextNote) { draftEvent = await createCommentDraftEvent(cleanedText, parentEvent, mentions, { addClientTag, protectedEvent: isProtectedEvent, - isNsfw + isNsfw, + addExpirationTag, + expirationMonths, + addQuietTag, + quietDays }) } else if (isPoll) { draftEvent = await createPollDraftEvent(pubkey!, cleanedText, mentions, pollCreateData, { addClientTag, - isNsfw + isNsfw, + addExpirationTag, + expirationMonths, + addQuietTag, + quietDays }) } else { draftEvent = await createShortTextNoteDraftEvent(cleanedText, mentions, { parentEvent, addClientTag, protectedEvent: isProtectedEvent, - isNsfw + isNsfw, + addExpirationTag, + expirationMonths, + addQuietTag, + quietDays }) } diff --git a/src/constants.ts b/src/constants.ts index 3148a37..a800297 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -46,6 +46,12 @@ export const StorageKey = { MEDIA_AUTO_LOAD_POLICY: 'mediaAutoLoadPolicy', SHOWN_CREATE_WALLET_GUIDE_TOAST_PUBKEYS: 'shownCreateWalletGuideToastPubkeys', SHOW_RECOMMENDED_RELAYS_PANEL: 'showRecommendedRelaysPanel', + DEFAULT_EXPIRATION_ENABLED: 'defaultExpirationEnabled', + DEFAULT_EXPIRATION_MONTHS: 'defaultExpirationMonths', + DEFAULT_QUIET_ENABLED: 'defaultQuietEnabled', + DEFAULT_QUIET_DAYS: 'defaultQuietDays', + RESPECT_QUIET_TAGS: 'respectQuietTags', + GLOBAL_QUIET_MODE: 'globalQuietMode', MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated diff --git a/src/lib/draft-event.ts b/src/lib/draft-event.ts index 5881d44..a9c848b 100644 --- a/src/lib/draft-event.ts +++ b/src/lib/draft-event.ts @@ -112,6 +112,10 @@ export async function createShortTextNoteDraftEvent( addClientTag?: boolean protectedEvent?: boolean isNsfw?: boolean + addExpirationTag?: boolean + expirationMonths?: number + addQuietTag?: boolean + quietDays?: number } = {} ): Promise { // Process content to prefix nostr addresses before other transformations @@ -158,6 +162,14 @@ export async function createShortTextNoteDraftEvent( tags.push(buildProtectedTag()) } + if (options.addExpirationTag && options.expirationMonths) { + tags.push(buildExpirationTag(options.expirationMonths)) + } + + if (options.addQuietTag && options.quietDays) { + tags.push(buildQuietTag(options.quietDays)) + } + const baseDraft = { kind: kinds.ShortTextNote, content: transformedEmojisContent, @@ -189,6 +201,10 @@ export async function createCommentDraftEvent( addClientTag?: boolean protectedEvent?: boolean isNsfw?: boolean + addExpirationTag?: boolean + expirationMonths?: number + addQuietTag?: boolean + quietDays?: number } = {} ): Promise { // Process content to prefix nostr addresses before other transformations @@ -256,6 +272,14 @@ export async function createCommentDraftEvent( tags.push(buildProtectedTag()) } + if (options.addExpirationTag && options.expirationMonths) { + tags.push(buildExpirationTag(options.expirationMonths)) + } + + if (options.addQuietTag && options.quietDays) { + tags.push(buildQuietTag(options.quietDays)) + } + const baseDraft = { kind: ExtendedKind.COMMENT, content: transformedEmojisContent, @@ -272,6 +296,10 @@ export async function createPublicMessageReplyDraftEvent( options: { addClientTag?: boolean isNsfw?: boolean + addExpirationTag?: boolean + expirationMonths?: number + addQuietTag?: boolean + quietDays?: number } = {} ): Promise { // Process content to prefix nostr addresses before other transformations @@ -321,6 +349,14 @@ export async function createPublicMessageReplyDraftEvent( tags.push(buildNsfwTag()) } + if (options.addExpirationTag && options.expirationMonths) { + tags.push(buildExpirationTag(options.expirationMonths)) + } + + if (options.addQuietTag && options.quietDays) { + tags.push(buildQuietTag(options.quietDays)) + } + // console.log('📝 Final public message reply draft tags:', { // pTags: tags.filter(tag => tag[0] === 'p'), // qTags: tags.filter(tag => tag[0] === 'q'), @@ -342,6 +378,10 @@ export async function createPublicMessageDraftEvent( options: { addClientTag?: boolean isNsfw?: boolean + addExpirationTag?: boolean + expirationMonths?: number + addQuietTag?: boolean + quietDays?: number } = {} ): Promise { // Process content to prefix nostr addresses before other transformations @@ -371,6 +411,14 @@ export async function createPublicMessageDraftEvent( tags.push(buildNsfwTag()) } + if (options.addExpirationTag && options.expirationMonths) { + tags.push(buildExpirationTag(options.expirationMonths)) + } + + if (options.addQuietTag && options.quietDays) { + tags.push(buildQuietTag(options.quietDays)) + } + const baseDraft = { kind: ExtendedKind.PUBLIC_MESSAGE, content: transformedEmojisContent, @@ -495,10 +543,18 @@ export async function createPollDraftEvent( { isMultipleChoice, relays, options, endsAt }: TPollCreateData, { addClientTag, - isNsfw + isNsfw, + addExpirationTag, + expirationMonths, + addQuietTag, + quietDays }: { addClientTag?: boolean isNsfw?: boolean + addExpirationTag?: boolean + expirationMonths?: number + addQuietTag?: boolean + quietDays?: number } = {} ): Promise { const { content: transformedEmojisContent, emojiTags } = transformCustomEmojisInContent(question) @@ -547,6 +603,14 @@ export async function createPollDraftEvent( tags.push(buildNsfwTag()) } + if (addExpirationTag && expirationMonths) { + tags.push(buildExpirationTag(expirationMonths)) + } + + if (addQuietTag && quietDays) { + tags.push(buildQuietTag(quietDays)) + } + const baseDraft = { content: transformedEmojisContent.trim(), kind: ExtendedKind.POLL, @@ -895,6 +959,16 @@ function buildProtectedTag() { return ['-'] } +function buildExpirationTag(months: number): string[] { + const expirationTime = dayjs().add(months, 'month').unix() + return ['expiration', expirationTime.toString()] +} + +function buildQuietTag(days: number): string[] { + const quietEndTime = dayjs().add(days, 'day').unix() + return ['quiet', quietEndTime.toString()] +} + function trimTagEnd(tag: string[]) { let endIndex = tag.length - 1 while (endIndex >= 0 && tag[endIndex] === '') { @@ -921,6 +995,10 @@ export async function createHighlightDraftEvent( options?: { addClientTag?: boolean isNsfw?: boolean + addExpirationTag?: boolean + expirationMonths?: number + addQuietTag?: boolean + quietDays?: number } ): Promise { const tags: string[][] = [] @@ -1046,6 +1124,14 @@ export async function createHighlightDraftEvent( tags.push(buildNsfwTag()) } + if (options?.addExpirationTag && options?.expirationMonths) { + tags.push(buildExpirationTag(options.expirationMonths)) + } + + if (options?.addQuietTag && options?.quietDays) { + tags.push(buildQuietTag(options.quietDays)) + } + return setDraftEventCache({ kind: 9802, // NIP-84 highlight kind tags, diff --git a/src/lib/event-filtering.ts b/src/lib/event-filtering.ts new file mode 100644 index 0000000..59cad35 --- /dev/null +++ b/src/lib/event-filtering.ts @@ -0,0 +1,62 @@ +import { Event } from 'nostr-tools' +import dayjs from 'dayjs' +import storage from '@/services/local-storage.service' + +/** + * Check if an event has expired based on its expiration tag + */ +export function isEventExpired(event: Event): boolean { + const expirationTag = event.tags.find(tag => tag[0] === 'expiration') + if (!expirationTag || !expirationTag[1]) { + return false + } + + const expirationTime = parseInt(expirationTag[1]) + if (isNaN(expirationTime)) { + return false + } + + return dayjs().unix() > expirationTime +} + +/** + * Check if an event is in quiet mode based on its quiet tag + */ +export function isEventInQuietMode(event: Event): boolean { + const quietTag = event.tags.find(tag => tag[0] === 'quiet') + if (!quietTag || !quietTag[1]) { + return false + } + + const quietEndTime = parseInt(quietTag[1]) + if (isNaN(quietEndTime)) { + return false + } + + return dayjs().unix() < quietEndTime +} + +/** + * Check if interactions should be hidden for an event based on quiet settings + */ +export function shouldHideInteractions(event: Event): boolean { + // Check global quiet mode first + if (storage.getGlobalQuietMode()) { + return true + } + + // Check if we should respect quiet tags + if (!storage.getRespectQuietTags()) { + return false + } + + // Check if the event is in quiet mode + return isEventInQuietMode(event) +} + +/** + * Check if an event should be filtered out completely (expired) + */ +export function shouldFilterEvent(event: Event): boolean { + return isEventExpired(event) +} diff --git a/src/pages/secondary/PostSettingsPage/ExpirationSettings.tsx b/src/pages/secondary/PostSettingsPage/ExpirationSettings.tsx new file mode 100644 index 0000000..6b83c8b --- /dev/null +++ b/src/pages/secondary/PostSettingsPage/ExpirationSettings.tsx @@ -0,0 +1,66 @@ +import { Label } from '@/components/ui/label' +import { Switch } from '@/components/ui/switch' +import { Input } from '@/components/ui/input' +import storage from '@/services/local-storage.service' +import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' + +export default function ExpirationSettings() { + const { t } = useTranslation() + const [enabled, setEnabled] = useState(false) + const [months, setMonths] = useState(6) + + useEffect(() => { + setEnabled(storage.getDefaultExpirationEnabled()) + setMonths(storage.getDefaultExpirationMonths()) + }, []) + + const handleEnabledChange = (checked: boolean) => { + setEnabled(checked) + storage.setDefaultExpirationEnabled(checked) + } + + const handleMonthsChange = (value: string) => { + const num = parseInt(value) + if (!isNaN(num) && num >= 0 && Number.isInteger(num)) { + setMonths(num) + storage.setDefaultExpirationMonths(num) + } + } + + return ( +
+
+
+ + +
+
+ {t('Posts will automatically include expiration tags')} +
+
+ + {enabled && ( +
+ + handleMonthsChange(e.target.value)} + className="w-24" + /> +
+ {t('Posts will expire after this many months')} +
+
+ )} +
+ ) +} diff --git a/src/pages/secondary/PostSettingsPage/QuietSettings.tsx b/src/pages/secondary/PostSettingsPage/QuietSettings.tsx new file mode 100644 index 0000000..f67cbb5 --- /dev/null +++ b/src/pages/secondary/PostSettingsPage/QuietSettings.tsx @@ -0,0 +1,108 @@ +import { Label } from '@/components/ui/label' +import { Switch } from '@/components/ui/switch' +import { Input } from '@/components/ui/input' +import storage from '@/services/local-storage.service' +import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' + +export default function QuietSettings() { + const { t } = useTranslation() + const [enabled, setEnabled] = useState(false) + const [days, setDays] = useState(7) + const [respectQuietTags, setRespectQuietTags] = useState(true) + const [globalQuietMode, setGlobalQuietMode] = useState(false) + + useEffect(() => { + setEnabled(storage.getDefaultQuietEnabled()) + setDays(storage.getDefaultQuietDays()) + setRespectQuietTags(storage.getRespectQuietTags()) + setGlobalQuietMode(storage.getGlobalQuietMode()) + }, []) + + const handleEnabledChange = (checked: boolean) => { + setEnabled(checked) + storage.setDefaultQuietEnabled(checked) + } + + const handleDaysChange = (value: string) => { + const num = parseInt(value) + if (!isNaN(num) && num >= 0 && Number.isInteger(num)) { + setDays(num) + storage.setDefaultQuietDays(num) + } + } + + const handleRespectQuietTagsChange = (checked: boolean) => { + setRespectQuietTags(checked) + storage.setRespectQuietTags(checked) + } + + const handleGlobalQuietModeChange = (checked: boolean) => { + setGlobalQuietMode(checked) + storage.setGlobalQuietMode(checked) + } + + return ( +
+
+
+ + +
+
+ {t('Posts will automatically include quiet tags')} +
+
+ + {enabled && ( +
+ + handleDaysChange(e.target.value)} + className="w-24" + /> +
+ {t('Posts will be quiet for this many days')} +
+
+ )} + +
+
+ + +
+
+ {t('Hide interactions on posts with quiet tags')} +
+
+ +
+
+ + +
+
+ {t('Hide interactions on all posts')} +
+
+
+ ) +} diff --git a/src/pages/secondary/PostSettingsPage/index.tsx b/src/pages/secondary/PostSettingsPage/index.tsx index da1424a..7258b09 100644 --- a/src/pages/secondary/PostSettingsPage/index.tsx +++ b/src/pages/secondary/PostSettingsPage/index.tsx @@ -2,14 +2,24 @@ import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import { forwardRef } from 'react' import { useTranslation } from 'react-i18next' import MediaUploadServiceSetting from './MediaUploadServiceSetting' +import ExpirationSettings from './ExpirationSettings' +import QuietSettings from './QuietSettings' const PostSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index?: number; hideTitlebar?: boolean }, ref) => { const { t } = useTranslation() return ( -
+
+
+

{t('Expiration Tags')}

+ +
+
+

{t('Quiet Tags')}

+ +
) diff --git a/src/services/local-storage.service.ts b/src/services/local-storage.service.ts index 97dd963..5e62127 100644 --- a/src/services/local-storage.service.ts +++ b/src/services/local-storage.service.ts @@ -51,6 +51,12 @@ class LocalStorageService { private mediaAutoLoadPolicy: TMediaAutoLoadPolicy = MEDIA_AUTO_LOAD_POLICY.ALWAYS private showRecommendedRelaysPanel: boolean = false private shownCreateWalletGuideToastPubkeys: Set = new Set() + private defaultExpirationEnabled: boolean = false + private defaultExpirationMonths: number = 6 + private defaultQuietEnabled: boolean = false + private defaultQuietDays: number = 7 + private respectQuietTags: boolean = true + private globalQuietMode: boolean = false constructor() { if (!LocalStorageService.instance) { @@ -218,6 +224,35 @@ class LocalStorageService { ? new Set(JSON.parse(shownCreateWalletGuideToastPubkeysStr)) : new Set() + // Initialize expiration and quiet settings + const defaultExpirationEnabledStr = window.localStorage.getItem(StorageKey.DEFAULT_EXPIRATION_ENABLED) + this.defaultExpirationEnabled = defaultExpirationEnabledStr === 'true' + + const defaultExpirationMonthsStr = window.localStorage.getItem(StorageKey.DEFAULT_EXPIRATION_MONTHS) + if (defaultExpirationMonthsStr) { + const num = parseInt(defaultExpirationMonthsStr) + if (!isNaN(num) && num >= 0 && Number.isInteger(num)) { + this.defaultExpirationMonths = num + } + } + + const defaultQuietEnabledStr = window.localStorage.getItem(StorageKey.DEFAULT_QUIET_ENABLED) + this.defaultQuietEnabled = defaultQuietEnabledStr === 'true' + + const defaultQuietDaysStr = window.localStorage.getItem(StorageKey.DEFAULT_QUIET_DAYS) + if (defaultQuietDaysStr) { + const num = parseInt(defaultQuietDaysStr) + if (!isNaN(num) && num >= 0 && Number.isInteger(num)) { + this.defaultQuietDays = num + } + } + + const respectQuietTagsStr = window.localStorage.getItem(StorageKey.RESPECT_QUIET_TAGS) + this.respectQuietTags = respectQuietTagsStr === null ? true : respectQuietTagsStr === 'true' + + const globalQuietModeStr = window.localStorage.getItem(StorageKey.GLOBAL_QUIET_MODE) + this.globalQuietMode = globalQuietModeStr === 'true' + // Clean up deprecated data window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP) window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP) @@ -519,6 +554,66 @@ class LocalStorageService { JSON.stringify(Array.from(this.shownCreateWalletGuideToastPubkeys)) ) } + + // Expiration settings + getDefaultExpirationEnabled() { + return this.defaultExpirationEnabled + } + + setDefaultExpirationEnabled(enabled: boolean) { + this.defaultExpirationEnabled = enabled + window.localStorage.setItem(StorageKey.DEFAULT_EXPIRATION_ENABLED, enabled.toString()) + } + + getDefaultExpirationMonths() { + return this.defaultExpirationMonths + } + + setDefaultExpirationMonths(months: number) { + if (Number.isInteger(months) && months >= 0) { + this.defaultExpirationMonths = months + window.localStorage.setItem(StorageKey.DEFAULT_EXPIRATION_MONTHS, months.toString()) + } + } + + // Quiet settings + getDefaultQuietEnabled() { + return this.defaultQuietEnabled + } + + setDefaultQuietEnabled(enabled: boolean) { + this.defaultQuietEnabled = enabled + window.localStorage.setItem(StorageKey.DEFAULT_QUIET_ENABLED, enabled.toString()) + } + + getDefaultQuietDays() { + return this.defaultQuietDays + } + + setDefaultQuietDays(days: number) { + if (Number.isInteger(days) && days >= 0) { + this.defaultQuietDays = days + window.localStorage.setItem(StorageKey.DEFAULT_QUIET_DAYS, days.toString()) + } + } + + getRespectQuietTags() { + return this.respectQuietTags + } + + setRespectQuietTags(respect: boolean) { + this.respectQuietTags = respect + window.localStorage.setItem(StorageKey.RESPECT_QUIET_TAGS, respect.toString()) + } + + getGlobalQuietMode() { + return this.globalQuietMode + } + + setGlobalQuietMode(enabled: boolean) { + this.globalQuietMode = enabled + window.localStorage.setItem(StorageKey.GLOBAL_QUIET_MODE, enabled.toString()) + } } const instance = new LocalStorageService()