diff --git a/src/components/AlertPopup.tsx b/src/components/AlertPopup.tsx
new file mode 100644
index 0000000..e9334ee
--- /dev/null
+++ b/src/components/AlertPopup.tsx
@@ -0,0 +1,72 @@
+import { createPortal } from 'react-dom'
+import { AlertPopupProps } from 'types'
+
+export const AlertPopup = ({
+ header,
+ label,
+ handleConfirm,
+ handleClose
+}: AlertPopupProps) => {
+ return createPortal(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
,
+ document.body
+ )
+}
diff --git a/src/components/Filters/BlogsFilter.tsx b/src/components/Filters/BlogsFilter.tsx
index 71b4098..5efc7d2 100644
--- a/src/components/Filters/BlogsFilter.tsx
+++ b/src/components/Filters/BlogsFilter.tsx
@@ -1,16 +1,11 @@
import { useAppSelector, useLocalStorage } from 'hooks'
import React from 'react'
-import {
- FilterOptions,
- ModeratedFilter,
- NSFWFilter,
- SortBy,
- WOTFilterOptions
-} from 'types'
+import { FilterOptions, ModeratedFilter, SortBy, WOTFilterOptions } from 'types'
import { DEFAULT_FILTER_OPTIONS } from 'utils'
import { Dropdown } from './Dropdown'
import { Option } from './Option'
import { Filter } from '.'
+import { NsfwFilterOptions } from './NsfwFilterOptions'
type Props = {
author?: string | undefined
@@ -115,19 +110,7 @@ export const BlogsFilter = React.memo(
{/* nsfw filter options */}
- {Object.values(NSFWFilter).map((item, index) => (
-
- ))}
+
{/* source filter options */}
diff --git a/src/components/Filters/ModsFilter.tsx b/src/components/Filters/ModsFilter.tsx
index 3d72d0c..44f182d 100644
--- a/src/components/Filters/ModsFilter.tsx
+++ b/src/components/Filters/ModsFilter.tsx
@@ -5,13 +5,13 @@ import {
SortBy,
ModeratedFilter,
WOTFilterOptions,
- NSFWFilter,
RepostFilter
} from 'types'
import { DEFAULT_FILTER_OPTIONS } from 'utils'
import { Filter } from '.'
import { Dropdown } from './Dropdown'
import { Option } from './Option'
+import { NsfwFilterOptions } from './NsfwFilterOptions'
type Props = {
author?: string | undefined
@@ -115,19 +115,7 @@ export const ModFilter = React.memo(
{/* nsfw filter options */}
- {Object.values(NSFWFilter).map((item, index) => (
-
- ))}
+
{/* repost filter options */}
diff --git a/src/components/Filters/NsfwFilterOptions.tsx b/src/components/Filters/NsfwFilterOptions.tsx
new file mode 100644
index 0000000..05ab906
--- /dev/null
+++ b/src/components/Filters/NsfwFilterOptions.tsx
@@ -0,0 +1,64 @@
+import { FilterOptions, NSFWFilter } from 'types'
+import { Option } from './Option'
+import { NsfwAlertPopup } from 'components/NsfwAlertPopup'
+import { useState } from 'react'
+import { useLocalStorage } from 'hooks'
+import { DEFAULT_FILTER_OPTIONS } from 'utils'
+
+interface NsfwFilterOptionsProps {
+ filterKey: string
+}
+
+export const NsfwFilterOptions = ({ filterKey }: NsfwFilterOptionsProps) => {
+ const [, setFilterOptions] = useLocalStorage(
+ filterKey,
+ DEFAULT_FILTER_OPTIONS
+ )
+ const [showNsfwPopup, setShowNsfwPopup] = useState(false)
+ const [selectedNsfwOption, setSelectedNsfwOption] = useState<
+ NSFWFilter | undefined
+ >()
+ const [confirmNsfw] = useLocalStorage('confirm-nsfw', false)
+ const handleConfirm = (confirm: boolean) => {
+ if (confirm && selectedNsfwOption) {
+ setFilterOptions((prev) => ({
+ ...prev,
+ nsfw: selectedNsfwOption
+ }))
+ }
+ }
+
+ return (
+ <>
+ {Object.values(NSFWFilter).map((item, index) => (
+
+ ))}
+ {showNsfwPopup && (
+ setShowNsfwPopup(false)}
+ />
+ )}
+ >
+ )
+}
diff --git a/src/components/NsfwAlertPopup.tsx b/src/components/NsfwAlertPopup.tsx
new file mode 100644
index 0000000..1685a97
--- /dev/null
+++ b/src/components/NsfwAlertPopup.tsx
@@ -0,0 +1,36 @@
+import { AlertPopupProps } from 'types'
+import { AlertPopup } from './AlertPopup'
+import { useLocalStorage } from 'hooks'
+
+type NsfwAlertPopup = Omit
+
+/**
+ * Triggers when the user wants to switch the filter to see any of the NSFW options
+ * (including preferences)
+ *
+ * Option will be remembered for the session only and will not show the popup again
+ */
+export const NsfwAlertPopup = ({
+ handleConfirm,
+ handleClose
+}: NsfwAlertPopup) => {
+ const [confirmNsfw, setConfirmNsfw] = useLocalStorage(
+ 'confirm-nsfw',
+ false
+ )
+
+ return (
+ !confirmNsfw && (
+ {
+ setConfirmNsfw(confirm)
+ handleConfirm(confirm)
+ handleClose()
+ }}
+ />
+ )
+ )
+}
diff --git a/src/components/ReportPopup.tsx b/src/components/ReportPopup.tsx
index d31e4b3..ace813d 100644
--- a/src/components/ReportPopup.tsx
+++ b/src/components/ReportPopup.tsx
@@ -3,12 +3,12 @@ import { CheckboxFieldUncontrolled } from 'components/Inputs'
import { useEffect } from 'react'
import { ReportReason } from 'types/report'
import { LoadingSpinner } from './LoadingSpinner'
+import { PopupProps } from 'types'
type ReportPopupProps = {
openedAt: number
reasons: ReportReason[]
- handleClose: () => void
-}
+} & PopupProps
export const ReportPopup = ({
openedAt,
diff --git a/src/hooks/index.ts b/src/hooks/index.ts
index 3daf9f4..c01237e 100644
--- a/src/hooks/index.ts
+++ b/src/hooks/index.ts
@@ -8,3 +8,4 @@ export * from './useReactions'
export * from './useNDKContext'
export * from './useScrollDisable'
export * from './useLocalStorage'
+export * from './useSessionStorage'
diff --git a/src/hooks/useSessionStorage.tsx b/src/hooks/useSessionStorage.tsx
new file mode 100644
index 0000000..8f50422
--- /dev/null
+++ b/src/hooks/useSessionStorage.tsx
@@ -0,0 +1,71 @@
+import React from 'react'
+import {
+ getSessionStorageItem,
+ removeSessionStorageItem,
+ setSessionStorageItem
+} from 'utils'
+
+const useSessionStorageSubscribe = (callback: () => void) => {
+ window.addEventListener('sessionStorage', callback)
+ return () => window.removeEventListener('sessionStorage', callback)
+}
+
+function mergeWithInitialValue(storedValue: T, initialValue: T): T {
+ if (typeof storedValue === 'object' && storedValue !== null) {
+ return { ...initialValue, ...storedValue }
+ }
+ return storedValue
+}
+
+export function useSessionStorage(
+ key: string,
+ initialValue: T
+): [T, React.Dispatch>] {
+ const getSnapshot = () => {
+ // Get the stored value
+ const storedValue = getSessionStorageItem(key, initialValue)
+
+ // Parse the value
+ const parsedStoredValue = JSON.parse(storedValue)
+
+ // Merge the default and the stored in case some of the required fields are missing
+ return JSON.stringify(
+ mergeWithInitialValue(parsedStoredValue, initialValue)
+ )
+ }
+
+ const data = React.useSyncExternalStore(
+ useSessionStorageSubscribe,
+ getSnapshot
+ )
+
+ const setState: React.Dispatch> = React.useCallback(
+ (v: React.SetStateAction) => {
+ try {
+ const nextState =
+ typeof v === 'function'
+ ? (v as (prevState: T) => T)(JSON.parse(data))
+ : v
+
+ if (nextState === undefined || nextState === null) {
+ removeSessionStorageItem(key)
+ } else {
+ setSessionStorageItem(key, JSON.stringify(nextState))
+ }
+ } catch (e) {
+ console.warn(e)
+ }
+ },
+ [data, key]
+ )
+
+ React.useEffect(() => {
+ // Set session storage only when it's empty
+ const data = window.sessionStorage.getItem(key)
+ if (data === null) {
+ setSessionStorageItem(key, JSON.stringify(initialValue))
+ }
+ }, [key, initialValue])
+
+ return [JSON.parse(data) as T, setState]
+}
diff --git a/src/pages/blogs/index.tsx b/src/pages/blogs/index.tsx
index 2155b39..d4b4fab 100644
--- a/src/pages/blogs/index.tsx
+++ b/src/pages/blogs/index.tsx
@@ -14,17 +14,16 @@ import { LoadingSpinner } from 'components/LoadingSpinner'
import { Filter } from 'components/Filters'
import { Dropdown } from 'components/Filters/Dropdown'
import { Option } from 'components/Filters/Option'
+import { NsfwFilterOptions } from 'components/Filters/NsfwFilterOptions'
export const BlogsPage = () => {
const navigation = useNavigation()
const blogs = useLoaderData() as Partial[] | undefined
- const [filterOptions, setFilterOptions] = useLocalStorage(
- 'filter-blog-curated',
- {
- sort: SortBy.Latest,
- nsfw: NSFWFilter.Hide_NSFW
- }
- )
+ const filterKey = 'filter-blog-curated'
+ const [filterOptions, setFilterOptions] = useLocalStorage(filterKey, {
+ sort: SortBy.Latest,
+ nsfw: NSFWFilter.Hide_NSFW
+ })
// Search
const searchTermRef = useRef(null)
@@ -147,19 +146,7 @@ export const BlogsPage = () => {
- {Object.values(NSFWFilter).map((item, index) => (
-
- ))}
+
diff --git a/src/pages/settings/preference.tsx b/src/pages/settings/preference.tsx
index 985a509..7f6c8e0 100644
--- a/src/pages/settings/preference.tsx
+++ b/src/pages/settings/preference.tsx
@@ -1,6 +1,12 @@
import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'
import { LoadingSpinner } from 'components/LoadingSpinner'
-import { useAppDispatch, useAppSelector, useNDKContext } from 'hooks'
+import { NsfwAlertPopup } from 'components/NsfwAlertPopup'
+import {
+ useAppDispatch,
+ useAppSelector,
+ useNDKContext,
+ useLocalStorage
+} from 'hooks'
import { kinds, UnsignedEvent, Event } from 'nostr-tools'
import { useEffect, useState } from 'react'
import { toast } from 'react-toastify'
@@ -19,6 +25,13 @@ export const PreferencesSetting = () => {
const [wotLevel, setWotLevel] = useState(userWotLevel)
const [isSaving, setIsSaving] = useState(false)
+ const [nsfw, setNsfw] = useState(false)
+ const [confirmNsfw] = useLocalStorage('confirm-nsfw', false)
+ const [showNsfwPopup, setShowNsfwPopup] = useState(false)
+ const handleNsfwConfirm = (confirm: boolean) => {
+ setNsfw(confirm)
+ }
+
useEffect(() => {
if (user?.pubkey) {
const hexPubkey = user.pubkey as string
@@ -191,6 +204,14 @@ export const PreferencesSetting = () => {
type='checkbox'
className='CheckboxMain'
name='NSFWPreference'
+ checked={nsfw}
+ onChange={(e) => {
+ if (e.currentTarget.checked && !confirmNsfw) {
+ setShowNsfwPopup(true)
+ } else {
+ setNsfw(e.currentTarget.checked)
+ }
+ }}
/>
@@ -238,6 +259,12 @@ export const PreferencesSetting = () => {
Save
+ {showNsfwPopup && (
+ setShowNsfwPopup(false)}
+ />
+ )}
diff --git a/src/types/index.ts b/src/types/index.ts
index d26ffe0..7b2f35c 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -5,3 +5,4 @@ export * from './user'
export * from './zap'
export * from './blog'
export * from './category'
+export * from './popup'
diff --git a/src/types/popup.ts b/src/types/popup.ts
new file mode 100644
index 0000000..b1551cf
--- /dev/null
+++ b/src/types/popup.ts
@@ -0,0 +1,9 @@
+export interface PopupProps {
+ handleClose: () => void
+}
+
+export interface AlertPopupProps extends PopupProps {
+ header: string
+ label: string
+ handleConfirm: (confirm: boolean) => void
+}
diff --git a/src/utils/index.ts b/src/utils/index.ts
index f6de84d..d8c7a4d 100644
--- a/src/utils/index.ts
+++ b/src/utils/index.ts
@@ -4,6 +4,7 @@ export * from './url'
export * from './utils'
export * from './zap'
export * from './localStorage'
+export * from './sessionStorage'
export * from './consts'
export * from './blog'
export * from './curationSets'
diff --git a/src/utils/sessionStorage.ts b/src/utils/sessionStorage.ts
new file mode 100644
index 0000000..e40b0e9
--- /dev/null
+++ b/src/utils/sessionStorage.ts
@@ -0,0 +1,32 @@
+export function getSessionStorageItem(key: string, defaultValue: T): string {
+ try {
+ const data = window.sessionStorage.getItem(key)
+ if (data === null) return JSON.stringify(defaultValue)
+ return data
+ } catch (err) {
+ console.error(`Error while fetching session storage value: `, err)
+ return JSON.stringify(defaultValue)
+ }
+}
+
+export function setSessionStorageItem(key: string, value: string) {
+ try {
+ window.sessionStorage.setItem(key, value)
+ dispatchSessionStorageEvent(key, value)
+ } catch (err) {
+ console.error(`Error while saving session storage value: `, err)
+ }
+}
+
+export function removeSessionStorageItem(key: string) {
+ try {
+ window.sessionStorage.removeItem(key)
+ dispatchSessionStorageEvent(key, null)
+ } catch (err) {
+ console.error(`Error while deleting session storage value: `, err)
+ }
+}
+
+function dispatchSessionStorageEvent(key: string, newValue: string | null) {
+ window.dispatchEvent(new StorageEvent('sessionStorage', { key, newValue }))
+}