Browse Source

quiet and expiration tags implemented

imwald
Silberengel 5 months ago
parent
commit
ed135d3dab
  1. 6
      src/components/NoteInteractions/index.tsx
  2. 4
      src/components/NoteList/index.tsx
  3. 6
      src/components/NoteStats/LikeButton.tsx
  4. 4
      src/components/NoteStats/ReplyButton.tsx
  5. 4
      src/components/NoteStats/RepostButton.tsx
  6. 4
      src/components/NoteStats/ZapButton.tsx
  7. 20
      src/components/NoteStats/index.tsx
  8. 43
      src/components/PostEditor/PostContent.tsx
  9. 6
      src/constants.ts
  10. 88
      src/lib/draft-event.ts
  11. 62
      src/lib/event-filtering.ts
  12. 66
      src/pages/secondary/PostSettingsPage/ExpirationSettings.tsx
  13. 108
      src/pages/secondary/PostSettingsPage/QuietSettings.tsx
  14. 12
      src/pages/secondary/PostSettingsPage/index.tsx
  15. 95
      src/services/local-storage.service.ts

6
src/components/NoteInteractions/index.tsx

@ -1,6 +1,7 @@
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area' import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { shouldHideInteractions } from '@/lib/event-filtering'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useState } from 'react' import { useState } from 'react'
import HideUntrustedContentButton from '../HideUntrustedContentButton' import HideUntrustedContentButton from '../HideUntrustedContentButton'
@ -23,6 +24,11 @@ export default function NoteInteractions({
const [replySort, setReplySort] = useState<ReplySortOption>('oldest') const [replySort, setReplySort] = useState<ReplySortOption>('oldest')
const isDiscussion = event.kind === ExtendedKind.DISCUSSION const isDiscussion = event.kind === ExtendedKind.DISCUSSION
// Hide interactions if event is in quiet mode
if (shouldHideInteractions(event)) {
return null
}
let list let list
switch (type) { switch (type) {
case 'replies': case 'replies':

4
src/components/NoteList/index.tsx

@ -6,6 +6,7 @@ import {
isReplaceableEvent, isReplaceableEvent,
isReplyNoteEvent isReplyNoteEvent
} from '@/lib/event' } from '@/lib/event'
import { shouldFilterEvent } from '@/lib/event-filtering'
import { isTouchDevice } from '@/lib/utils' import { isTouchDevice } from '@/lib/utils'
import { useContentPolicy } from '@/providers/ContentPolicyProvider' import { useContentPolicy } from '@/providers/ContentPolicyProvider'
import { useDeletedEvent } from '@/providers/DeletedEventProvider' import { useDeletedEvent } from '@/providers/DeletedEventProvider'
@ -102,6 +103,9 @@ const NoteList = forwardRef(
return true return true
} }
// Filter out expired events
if (shouldFilterEvent(evt)) return true
return false return false
}, },
[hideReplies, hideUntrustedNotes, mutePubkeySet, pinnedEventIds, isEventDeleted] [hideReplies, hideUntrustedNotes, mutePubkeySet, pinnedEventIds, isEventDeleted]

6
src/components/NoteStats/LikeButton.tsx

@ -24,7 +24,7 @@ import SuggestedEmojis from '../SuggestedEmojis'
import DiscussionEmojis from '../SuggestedEmojis/DiscussionEmojis' import DiscussionEmojis from '../SuggestedEmojis/DiscussionEmojis'
import { formatCount } from './utils' 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 { t } = useTranslation()
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
const { pubkey, publish, checkLogin } = useNostr() const { pubkey, publish, checkLogin } = useNostr()
@ -144,12 +144,12 @@ export default function LikeButton({ event }: { event: Event }) {
) : myLastEmoji ? ( ) : myLastEmoji ? (
<> <>
<Emoji emoji={myLastEmoji} classNames={{ img: 'size-4' }} /> <Emoji emoji={myLastEmoji} classNames={{ img: 'size-4' }} />
{!!likeCount && <div className="text-sm">{formatCount(likeCount)}</div>} {!hideCount && !!likeCount && <div className="text-sm">{formatCount(likeCount)}</div>}
</> </>
) : ( ) : (
<> <>
<SmilePlus /> <SmilePlus />
{!!likeCount && <div className="text-sm">{formatCount(likeCount)}</div>} {!hideCount && !!likeCount && <div className="text-sm">{formatCount(likeCount)}</div>}
</> </>
)} )}
</button> </button>

4
src/components/NoteStats/ReplyButton.tsx

@ -9,7 +9,7 @@ import { useTranslation } from 'react-i18next'
import PostEditor from '../PostEditor' import PostEditor from '../PostEditor'
import { formatCount } from './utils' 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 { t } = useTranslation()
const { pubkey, checkLogin } = useNostr() const { pubkey, checkLogin } = useNostr()
const noteStats = useNoteStatsById(event.id) const noteStats = useNoteStatsById(event.id)
@ -44,7 +44,7 @@ export default function ReplyButton({ event }: { event: Event }) {
title={t('Reply')} title={t('Reply')}
> >
<MessageCircle /> <MessageCircle />
{!!replyCount && <div className="text-sm">{formatCount(replyCount)}</div>} {!hideCount && !!replyCount && <div className="text-sm">{formatCount(replyCount)}</div>}
</button> </button>
<PostEditor parentEvent={event} open={open} setOpen={setOpen} /> <PostEditor parentEvent={event} open={open} setOpen={setOpen} />
</> </>

4
src/components/NoteStats/RepostButton.tsx

@ -21,7 +21,7 @@ import { useTranslation } from 'react-i18next'
import PostEditor from '../PostEditor' import PostEditor from '../PostEditor'
import { formatCount } from './utils' 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 { t } = useTranslation()
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
const { hideUntrustedInteractions, isUserTrusted } = useUserTrust() const { hideUntrustedInteractions, isUserTrusted } = useUserTrust()
@ -82,7 +82,7 @@ export default function RepostButton({ event }: { event: Event }) {
}} }}
> >
{reposting ? <Loader className="animate-spin" /> : <Repeat />} {reposting ? <Loader className="animate-spin" /> : <Repeat />}
{!!repostCount && <div className="text-sm">{formatCount(repostCount)}</div>} {!hideCount && !!repostCount && <div className="text-sm">{formatCount(repostCount)}</div>}
</button> </button>
) )

4
src/components/NoteStats/ZapButton.tsx

@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next'
import { toast } from 'sonner' import { toast } from 'sonner'
import ZapDialog from '../ZapDialog' 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 { t } = useTranslation()
const { checkLogin, pubkey } = useNostr() const { checkLogin, pubkey } = useNostr()
const noteStats = useNoteStatsById(event.id) const noteStats = useNoteStatsById(event.id)
@ -147,7 +147,7 @@ export default function ZapButton({ event }: { event: Event }) {
) : ( ) : (
<Zap className={hasZapped ? 'fill-yellow-400' : ''} /> <Zap className={hasZapped ? 'fill-yellow-400' : ''} />
)} )}
{!!zapAmount && <div className="text-sm">{formatAmount(zapAmount)}</div>} {!hideCount && !!zapAmount && <div className="text-sm">{formatAmount(zapAmount)}</div>}
</button> </button>
<ZapDialog <ZapDialog
open={openZapDialog} open={openZapDialog}

20
src/components/NoteStats/index.tsx

@ -5,6 +5,7 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import noteStatsService from '@/services/note-stats.service' import noteStatsService from '@/services/note-stats.service'
import { ExtendedKind } from '@/constants' import { ExtendedKind } from '@/constants'
import { getRootEventHexId } from '@/lib/event' import { getRootEventHexId } from '@/lib/event'
import { shouldHideInteractions } from '@/lib/event-filtering'
import client from '@/services/client.service' import client from '@/services/client.service'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import { useEffect, useState, useMemo } from 'react' import { useEffect, useState, useMemo } from 'react'
@ -41,6 +42,9 @@ export default function NoteStats({
const isDiscussion = event.kind === ExtendedKind.DISCUSSION const isDiscussion = event.kind === ExtendedKind.DISCUSSION
const [isReplyToDiscussion, setIsReplyToDiscussion] = useState(false) const [isReplyToDiscussion, setIsReplyToDiscussion] = useState(false)
// Hide interaction counts if event is in quiet mode
const hideInteractions = shouldHideInteractions(event)
useMemo(() => { useMemo(() => {
if (isDiscussion) return // Already a discussion event if (isDiscussion) return // Already a discussion event
@ -80,10 +84,10 @@ export default function NoteStats({
classNames?.buttonBar classNames?.buttonBar
)} )}
> >
<ReplyButton event={event} /> <ReplyButton event={event} hideCount={hideInteractions} />
{!isDiscussion && !isReplyToDiscussion && <RepostButton event={event} />} {!isDiscussion && !isReplyToDiscussion && <RepostButton event={event} hideCount={hideInteractions} />}
<LikeButton event={event} /> <LikeButton event={event} hideCount={hideInteractions} />
<ZapButton event={event} /> <ZapButton event={event} hideCount={hideInteractions} />
<BookmarkButton event={event} /> <BookmarkButton event={event} />
<SeenOnButton event={event} /> <SeenOnButton event={event} />
</div> </div>
@ -103,10 +107,10 @@ export default function NoteStats({
<div <div
className={cn('flex items-center', loading ? 'animate-pulse' : '')} className={cn('flex items-center', loading ? 'animate-pulse' : '')}
> >
<ReplyButton event={event} /> <ReplyButton event={event} hideCount={hideInteractions} />
{!isDiscussion && !isReplyToDiscussion && <RepostButton event={event} />} {!isDiscussion && !isReplyToDiscussion && <RepostButton event={event} hideCount={hideInteractions} />}
<LikeButton event={event} /> <LikeButton event={event} hideCount={hideInteractions} />
<ZapButton event={event} /> <ZapButton event={event} hideCount={hideInteractions} />
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<BookmarkButton event={event} /> <BookmarkButton event={event} />

43
src/components/PostEditor/PostContent.tsx

@ -17,6 +17,7 @@ import { useFeed } from '@/providers/FeedProvider'
import { useReply } from '@/providers/ReplyProvider' import { useReply } from '@/providers/ReplyProvider'
import { normalizeUrl, cleanUrl } from '@/lib/url' import { normalizeUrl, cleanUrl } from '@/lib/url'
import postEditorCache from '@/services/post-editor-cache.service' import postEditorCache from '@/services/post-editor-cache.service'
import storage from '@/services/local-storage.service'
import { TPollCreateData } from '@/types' import { TPollCreateData } from '@/types'
import { ImageUp, ListTodo, LoaderCircle, MessageCircle, Settings, Smile, X, Highlighter } from 'lucide-react' import { ImageUp, ListTodo, LoaderCircle, MessageCircle, Settings, Smile, X, Highlighter } from 'lucide-react'
import { Event, kinds } from 'nostr-tools' 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) { if (isHighlight) {
// For highlights, pass the original sourceValue which contains the full identifier // For highlights, pass the original sourceValue which contains the full identifier
// The createHighlightDraftEvent function will parse it correctly // The createHighlightDraftEvent function will parse it correctly
@ -213,36 +220,60 @@ export default function PostContent({
undefined, // description parameter (not used) undefined, // description parameter (not used)
{ {
addClientTag, addClientTag,
isNsfw isNsfw,
addExpirationTag,
expirationMonths,
addQuietTag,
quietDays
} }
) )
} else if (isPublicMessage) { } else if (isPublicMessage) {
draftEvent = await createPublicMessageDraftEvent(cleanedText, extractedMentions, { draftEvent = await createPublicMessageDraftEvent(cleanedText, extractedMentions, {
addClientTag, addClientTag,
isNsfw isNsfw,
addExpirationTag,
expirationMonths,
addQuietTag,
quietDays
}) })
} else if (parentEvent && parentEvent.kind === ExtendedKind.PUBLIC_MESSAGE) { } else if (parentEvent && parentEvent.kind === ExtendedKind.PUBLIC_MESSAGE) {
draftEvent = await createPublicMessageReplyDraftEvent(cleanedText, parentEvent, mentions, { draftEvent = await createPublicMessageReplyDraftEvent(cleanedText, parentEvent, mentions, {
addClientTag, addClientTag,
isNsfw isNsfw,
addExpirationTag,
expirationMonths,
addQuietTag,
quietDays
}) })
} else if (parentEvent && parentEvent.kind !== kinds.ShortTextNote) { } else if (parentEvent && parentEvent.kind !== kinds.ShortTextNote) {
draftEvent = await createCommentDraftEvent(cleanedText, parentEvent, mentions, { draftEvent = await createCommentDraftEvent(cleanedText, parentEvent, mentions, {
addClientTag, addClientTag,
protectedEvent: isProtectedEvent, protectedEvent: isProtectedEvent,
isNsfw isNsfw,
addExpirationTag,
expirationMonths,
addQuietTag,
quietDays
}) })
} else if (isPoll) { } else if (isPoll) {
draftEvent = await createPollDraftEvent(pubkey!, cleanedText, mentions, pollCreateData, { draftEvent = await createPollDraftEvent(pubkey!, cleanedText, mentions, pollCreateData, {
addClientTag, addClientTag,
isNsfw isNsfw,
addExpirationTag,
expirationMonths,
addQuietTag,
quietDays
}) })
} else { } else {
draftEvent = await createShortTextNoteDraftEvent(cleanedText, mentions, { draftEvent = await createShortTextNoteDraftEvent(cleanedText, mentions, {
parentEvent, parentEvent,
addClientTag, addClientTag,
protectedEvent: isProtectedEvent, protectedEvent: isProtectedEvent,
isNsfw isNsfw,
addExpirationTag,
expirationMonths,
addQuietTag,
quietDays
}) })
} }

6
src/constants.ts

@ -46,6 +46,12 @@ export const StorageKey = {
MEDIA_AUTO_LOAD_POLICY: 'mediaAutoLoadPolicy', MEDIA_AUTO_LOAD_POLICY: 'mediaAutoLoadPolicy',
SHOWN_CREATE_WALLET_GUIDE_TOAST_PUBKEYS: 'shownCreateWalletGuideToastPubkeys', SHOWN_CREATE_WALLET_GUIDE_TOAST_PUBKEYS: 'shownCreateWalletGuideToastPubkeys',
SHOW_RECOMMENDED_RELAYS_PANEL: 'showRecommendedRelaysPanel', 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 MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated
HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated
ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated

88
src/lib/draft-event.ts

@ -112,6 +112,10 @@ export async function createShortTextNoteDraftEvent(
addClientTag?: boolean addClientTag?: boolean
protectedEvent?: boolean protectedEvent?: boolean
isNsfw?: boolean isNsfw?: boolean
addExpirationTag?: boolean
expirationMonths?: number
addQuietTag?: boolean
quietDays?: number
} = {} } = {}
): Promise<TDraftEvent> { ): Promise<TDraftEvent> {
// Process content to prefix nostr addresses before other transformations // Process content to prefix nostr addresses before other transformations
@ -158,6 +162,14 @@ export async function createShortTextNoteDraftEvent(
tags.push(buildProtectedTag()) 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 = { const baseDraft = {
kind: kinds.ShortTextNote, kind: kinds.ShortTextNote,
content: transformedEmojisContent, content: transformedEmojisContent,
@ -189,6 +201,10 @@ export async function createCommentDraftEvent(
addClientTag?: boolean addClientTag?: boolean
protectedEvent?: boolean protectedEvent?: boolean
isNsfw?: boolean isNsfw?: boolean
addExpirationTag?: boolean
expirationMonths?: number
addQuietTag?: boolean
quietDays?: number
} = {} } = {}
): Promise<TDraftEvent> { ): Promise<TDraftEvent> {
// Process content to prefix nostr addresses before other transformations // Process content to prefix nostr addresses before other transformations
@ -256,6 +272,14 @@ export async function createCommentDraftEvent(
tags.push(buildProtectedTag()) 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 = { const baseDraft = {
kind: ExtendedKind.COMMENT, kind: ExtendedKind.COMMENT,
content: transformedEmojisContent, content: transformedEmojisContent,
@ -272,6 +296,10 @@ export async function createPublicMessageReplyDraftEvent(
options: { options: {
addClientTag?: boolean addClientTag?: boolean
isNsfw?: boolean isNsfw?: boolean
addExpirationTag?: boolean
expirationMonths?: number
addQuietTag?: boolean
quietDays?: number
} = {} } = {}
): Promise<TDraftEvent> { ): Promise<TDraftEvent> {
// Process content to prefix nostr addresses before other transformations // Process content to prefix nostr addresses before other transformations
@ -321,6 +349,14 @@ export async function createPublicMessageReplyDraftEvent(
tags.push(buildNsfwTag()) 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:', { // console.log('📝 Final public message reply draft tags:', {
// pTags: tags.filter(tag => tag[0] === 'p'), // pTags: tags.filter(tag => tag[0] === 'p'),
// qTags: tags.filter(tag => tag[0] === 'q'), // qTags: tags.filter(tag => tag[0] === 'q'),
@ -342,6 +378,10 @@ export async function createPublicMessageDraftEvent(
options: { options: {
addClientTag?: boolean addClientTag?: boolean
isNsfw?: boolean isNsfw?: boolean
addExpirationTag?: boolean
expirationMonths?: number
addQuietTag?: boolean
quietDays?: number
} = {} } = {}
): Promise<TDraftEvent> { ): Promise<TDraftEvent> {
// Process content to prefix nostr addresses before other transformations // Process content to prefix nostr addresses before other transformations
@ -371,6 +411,14 @@ export async function createPublicMessageDraftEvent(
tags.push(buildNsfwTag()) 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 = { const baseDraft = {
kind: ExtendedKind.PUBLIC_MESSAGE, kind: ExtendedKind.PUBLIC_MESSAGE,
content: transformedEmojisContent, content: transformedEmojisContent,
@ -495,10 +543,18 @@ export async function createPollDraftEvent(
{ isMultipleChoice, relays, options, endsAt }: TPollCreateData, { isMultipleChoice, relays, options, endsAt }: TPollCreateData,
{ {
addClientTag, addClientTag,
isNsfw isNsfw,
addExpirationTag,
expirationMonths,
addQuietTag,
quietDays
}: { }: {
addClientTag?: boolean addClientTag?: boolean
isNsfw?: boolean isNsfw?: boolean
addExpirationTag?: boolean
expirationMonths?: number
addQuietTag?: boolean
quietDays?: number
} = {} } = {}
): Promise<TDraftEvent> { ): Promise<TDraftEvent> {
const { content: transformedEmojisContent, emojiTags } = transformCustomEmojisInContent(question) const { content: transformedEmojisContent, emojiTags } = transformCustomEmojisInContent(question)
@ -547,6 +603,14 @@ export async function createPollDraftEvent(
tags.push(buildNsfwTag()) tags.push(buildNsfwTag())
} }
if (addExpirationTag && expirationMonths) {
tags.push(buildExpirationTag(expirationMonths))
}
if (addQuietTag && quietDays) {
tags.push(buildQuietTag(quietDays))
}
const baseDraft = { const baseDraft = {
content: transformedEmojisContent.trim(), content: transformedEmojisContent.trim(),
kind: ExtendedKind.POLL, kind: ExtendedKind.POLL,
@ -895,6 +959,16 @@ function buildProtectedTag() {
return ['-'] 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[]) { function trimTagEnd(tag: string[]) {
let endIndex = tag.length - 1 let endIndex = tag.length - 1
while (endIndex >= 0 && tag[endIndex] === '') { while (endIndex >= 0 && tag[endIndex] === '') {
@ -921,6 +995,10 @@ export async function createHighlightDraftEvent(
options?: { options?: {
addClientTag?: boolean addClientTag?: boolean
isNsfw?: boolean isNsfw?: boolean
addExpirationTag?: boolean
expirationMonths?: number
addQuietTag?: boolean
quietDays?: number
} }
): Promise<TDraftEvent> { ): Promise<TDraftEvent> {
const tags: string[][] = [] const tags: string[][] = []
@ -1046,6 +1124,14 @@ export async function createHighlightDraftEvent(
tags.push(buildNsfwTag()) 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({ return setDraftEventCache({
kind: 9802, // NIP-84 highlight kind kind: 9802, // NIP-84 highlight kind
tags, tags,

62
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)
}

66
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 (
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Label htmlFor="expiration-enabled">{t('Add expiration tags by default')}</Label>
<Switch
id="expiration-enabled"
checked={enabled}
onCheckedChange={handleEnabledChange}
/>
</div>
<div className="text-muted-foreground text-xs">
{t('Posts will automatically include expiration tags')}
</div>
</div>
{enabled && (
<div className="space-y-2">
<Label htmlFor="expiration-months">{t('Default expiration (months)')}</Label>
<Input
id="expiration-months"
type="number"
min="0"
step="1"
value={months}
onChange={(e) => handleMonthsChange(e.target.value)}
className="w-24"
/>
<div className="text-muted-foreground text-xs">
{t('Posts will expire after this many months')}
</div>
</div>
)}
</div>
)
}

108
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 (
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Label htmlFor="quiet-enabled">{t('Add quiet tags by default')}</Label>
<Switch
id="quiet-enabled"
checked={enabled}
onCheckedChange={handleEnabledChange}
/>
</div>
<div className="text-muted-foreground text-xs">
{t('Posts will automatically include quiet tags')}
</div>
</div>
{enabled && (
<div className="space-y-2">
<Label htmlFor="quiet-days">{t('Default quiet period (days)')}</Label>
<Input
id="quiet-days"
type="number"
min="0"
step="1"
value={days}
onChange={(e) => handleDaysChange(e.target.value)}
className="w-24"
/>
<div className="text-muted-foreground text-xs">
{t('Posts will be quiet for this many days')}
</div>
</div>
)}
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Label htmlFor="respect-quiet-tags">{t('Respect quiet tags')}</Label>
<Switch
id="respect-quiet-tags"
checked={respectQuietTags}
onCheckedChange={handleRespectQuietTagsChange}
/>
</div>
<div className="text-muted-foreground text-xs">
{t('Hide interactions on posts with quiet tags')}
</div>
</div>
<div className="space-y-2">
<div className="flex items-center space-x-2">
<Label htmlFor="global-quiet-mode">{t('Global quiet mode')}</Label>
<Switch
id="global-quiet-mode"
checked={globalQuietMode}
onCheckedChange={handleGlobalQuietModeChange}
/>
</div>
<div className="text-muted-foreground text-xs">
{t('Hide interactions on all posts')}
</div>
</div>
</div>
)
}

12
src/pages/secondary/PostSettingsPage/index.tsx

@ -2,14 +2,24 @@ import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { forwardRef } from 'react' import { forwardRef } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import MediaUploadServiceSetting from './MediaUploadServiceSetting' import MediaUploadServiceSetting from './MediaUploadServiceSetting'
import ExpirationSettings from './ExpirationSettings'
import QuietSettings from './QuietSettings'
const PostSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index?: number; hideTitlebar?: boolean }, ref) => { const PostSettingsPage = forwardRef(({ index, hideTitlebar = false }: { index?: number; hideTitlebar?: boolean }, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<SecondaryPageLayout ref={ref} index={index} title={hideTitlebar ? undefined : t('Post settings')}> <SecondaryPageLayout ref={ref} index={index} title={hideTitlebar ? undefined : t('Post settings')}>
<div className="px-4 pt-3 space-y-4"> <div className="px-4 pt-3 space-y-6">
<MediaUploadServiceSetting /> <MediaUploadServiceSetting />
<div className="space-y-4">
<h3 className="text-lg font-medium">{t('Expiration Tags')}</h3>
<ExpirationSettings />
</div>
<div className="space-y-4">
<h3 className="text-lg font-medium">{t('Quiet Tags')}</h3>
<QuietSettings />
</div>
</div> </div>
</SecondaryPageLayout> </SecondaryPageLayout>
) )

95
src/services/local-storage.service.ts

@ -51,6 +51,12 @@ class LocalStorageService {
private mediaAutoLoadPolicy: TMediaAutoLoadPolicy = MEDIA_AUTO_LOAD_POLICY.ALWAYS private mediaAutoLoadPolicy: TMediaAutoLoadPolicy = MEDIA_AUTO_LOAD_POLICY.ALWAYS
private showRecommendedRelaysPanel: boolean = false private showRecommendedRelaysPanel: boolean = false
private shownCreateWalletGuideToastPubkeys: Set<string> = new Set() private shownCreateWalletGuideToastPubkeys: Set<string> = 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() { constructor() {
if (!LocalStorageService.instance) { if (!LocalStorageService.instance) {
@ -218,6 +224,35 @@ class LocalStorageService {
? new Set(JSON.parse(shownCreateWalletGuideToastPubkeysStr)) ? new Set(JSON.parse(shownCreateWalletGuideToastPubkeysStr))
: new Set() : 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 // Clean up deprecated data
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP) window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP) window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
@ -519,6 +554,66 @@ class LocalStorageService {
JSON.stringify(Array.from(this.shownCreateWalletGuideToastPubkeys)) 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() const instance = new LocalStorageService()

Loading…
Cancel
Save